@sovovs/bycli 2.1.40 → 2.1.42

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 (42) hide show
  1. package/cli-manifest.json +10 -2
  2. package/clis/weixin/_wechat/article-artifact.js +55 -0
  3. package/clis/weixin/_wechat/article-identity.js +27 -0
  4. package/clis/weixin/_wechat/publish-analysis.js +8 -4
  5. package/clis/weixin/_wechat/publish-download.js +3 -1
  6. package/clis/weixin/download-publish-data.js +20 -0
  7. package/clis/weixin/download.js +15 -2
  8. package/dist/src/adapter-coordination.d.ts +26 -0
  9. package/dist/src/adapter-coordination.js +183 -0
  10. package/dist/src/adapter-coordination.test.d.ts +1 -0
  11. package/dist/src/adapter-execution-context.d.ts +6 -0
  12. package/dist/src/adapter-execution-context.js +8 -0
  13. package/dist/src/adapter-scheduler.d.ts +86 -0
  14. package/dist/src/adapter-scheduler.js +349 -0
  15. package/dist/src/adapter-scheduler.test.d.ts +1 -0
  16. package/dist/src/browser/daemon-client.d.ts +11 -0
  17. package/dist/src/browser/daemon-client.js +53 -1
  18. package/dist/src/browser/extension-capabilities.d.ts +1 -0
  19. package/dist/src/browser/extension-capabilities.js +18 -5
  20. package/dist/src/browser/page.d.ts +2 -1
  21. package/dist/src/browser/page.js +3 -0
  22. package/dist/src/build-manifest.js +1 -0
  23. package/dist/src/cli-argv-preprocess.d.ts +3 -0
  24. package/dist/src/cli-argv-preprocess.js +4 -0
  25. package/dist/src/commanderAdapter.js +11 -0
  26. package/dist/src/daemon.js +118 -0
  27. package/dist/src/discovery.js +1 -0
  28. package/dist/src/download/article-download.d.ts +2 -0
  29. package/dist/src/download/article-download.js +30 -4
  30. package/dist/src/errors.d.ts +3 -0
  31. package/dist/src/errors.js +5 -0
  32. package/dist/src/execution.d.ts +2 -0
  33. package/dist/src/execution.js +172 -105
  34. package/dist/src/help.d.ts +1 -0
  35. package/dist/src/help.js +40 -0
  36. package/dist/src/manifest-types.d.ts +4 -0
  37. package/dist/src/registry.d.ts +6 -0
  38. package/dist/src/registry.js +23 -0
  39. package/dist/src/serialization.d.ts +1 -0
  40. package/dist/src/serialization.js +1 -0
  41. package/dist/src/types.d.ts +2 -0
  42. package/package.json +3 -2
@@ -0,0 +1,349 @@
1
+ import * as crypto from 'node:crypto';
2
+ export class AdapterSchedulerError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = 'AdapterSchedulerError';
8
+ }
9
+ }
10
+ export class AdapterScheduler {
11
+ now;
12
+ leaseExpiryMs;
13
+ runtimeCeiling;
14
+ releasedLeaseRetentionMs;
15
+ pools = new Map();
16
+ releasedLeaseIds = new Map();
17
+ generations = new Map();
18
+ resourceOwners = new Map();
19
+ resourceGrants = new Map();
20
+ resourceQueue = [];
21
+ sequence = 0;
22
+ constructor(options = {}) {
23
+ this.now = options.now ?? Date.now;
24
+ this.leaseExpiryMs = options.leaseExpiryMs ?? 45_000;
25
+ this.runtimeCeiling = options.runtimeCeiling ?? 3;
26
+ this.releasedLeaseRetentionMs = options.releasedLeaseRetentionMs ?? 300_000;
27
+ }
28
+ acquire(request) {
29
+ this.validateRequest(request);
30
+ const pool = this.getOrCreatePool(request);
31
+ if (pool.closed) {
32
+ return Promise.reject(this.poolClosedError(pool.closed));
33
+ }
34
+ pool.maxParallel = Math.min(pool.maxParallel, request.maxParallel, this.runtimeCeiling);
35
+ if ([...pool.running.values()].some(lease => lease.requestId === request.requestId)
36
+ || pool.queued.some(entry => entry.request.requestId === request.requestId)) {
37
+ return Promise.reject(new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Duplicate Adapter lease request id'));
38
+ }
39
+ return new Promise((resolve, reject) => {
40
+ const enqueuedAt = this.now();
41
+ const pending = {
42
+ request,
43
+ enqueuedAt,
44
+ deadline: enqueuedAt + request.queueTimeoutMs,
45
+ sequence: ++this.sequence,
46
+ resolve,
47
+ reject,
48
+ };
49
+ pending.timer = setTimeout(() => this.sweepExpired(), request.queueTimeoutMs);
50
+ pending.timer.unref?.();
51
+ pool.queued.push(pending);
52
+ this.schedule(pool);
53
+ });
54
+ }
55
+ heartbeat(identity) {
56
+ const lease = this.requireLease(identity);
57
+ lease.heartbeatDeadline = this.now() + this.leaseExpiryMs;
58
+ return { ...lease };
59
+ }
60
+ assertLease(identity) {
61
+ return { ...this.requireLease(identity) };
62
+ }
63
+ release(release) {
64
+ this.pruneReleasedLeaseIds();
65
+ if (this.releasedLeaseIds.has(release.leaseId))
66
+ return false;
67
+ const lease = this.requireLease(release);
68
+ const pool = this.pools.get(lease.poolKey);
69
+ this.releaseAllResourcesForLease(lease.leaseId);
70
+ this.rejectResourceWaitersForLease(lease.leaseId);
71
+ pool.running.delete(lease.leaseId);
72
+ pool.activeSessions.delete(lease.adapterSession);
73
+ this.releasedLeaseIds.set(lease.leaseId, this.now() + this.releasedLeaseRetentionMs);
74
+ if (release.reason === 'auth_gate' || release.reason === 'rate_limited') {
75
+ pool.closed = release.reason;
76
+ const error = this.poolClosedError(release.reason);
77
+ for (const pending of pool.queued.splice(0)) {
78
+ if (pending.timer)
79
+ clearTimeout(pending.timer);
80
+ pending.reject(error);
81
+ }
82
+ }
83
+ else {
84
+ this.schedule(pool);
85
+ }
86
+ this.scheduleResources();
87
+ this.removeDrainedPool(pool);
88
+ return true;
89
+ }
90
+ acquireResources(leaseIdentity, rawKeys, timeoutMs) {
91
+ const lease = this.requireLease(leaseIdentity);
92
+ const keys = [...new Set(rawKeys.map(key => key.trim()).filter(Boolean))].sort();
93
+ if (keys.length === 0) {
94
+ return Promise.reject(new AdapterSchedulerError('ADAPTER_RESOURCE_TIMEOUT', 'At least one resource key is required'));
95
+ }
96
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
97
+ return Promise.reject(new AdapterSchedulerError('ADAPTER_RESOURCE_TIMEOUT', 'Invalid Adapter resource timeout'));
98
+ }
99
+ return new Promise((resolve, reject) => {
100
+ const pending = {
101
+ lease: { ...lease },
102
+ keys,
103
+ deadline: this.now() + timeoutMs,
104
+ sequence: ++this.sequence,
105
+ resolve,
106
+ reject,
107
+ };
108
+ pending.timer = setTimeout(() => this.sweepExpired(), timeoutMs);
109
+ pending.timer.unref?.();
110
+ this.resourceQueue.push(pending);
111
+ this.scheduleResources();
112
+ });
113
+ }
114
+ releaseResources(leaseIdentity, grantId) {
115
+ const lease = this.requireLease(leaseIdentity);
116
+ const grant = this.resourceGrants.get(grantId);
117
+ if (!grant || grant.leaseId !== lease.leaseId) {
118
+ throw new AdapterSchedulerError('ADAPTER_LEASE_LOST', 'Adapter resource grant is no longer valid');
119
+ }
120
+ this.resourceGrants.delete(grantId);
121
+ for (const key of grant.keys) {
122
+ const owner = this.resourceOwners.get(key);
123
+ if (owner?.grantId === grantId)
124
+ this.resourceOwners.delete(key);
125
+ }
126
+ this.scheduleResources();
127
+ return true;
128
+ }
129
+ cancel(requestId, code = 'ADAPTER_QUEUE_RESET') {
130
+ for (const pool of this.pools.values()) {
131
+ const index = pool.queued.findIndex(entry => entry.request.requestId === requestId);
132
+ if (index === -1)
133
+ continue;
134
+ const [pending] = pool.queued.splice(index, 1);
135
+ if (pending.timer)
136
+ clearTimeout(pending.timer);
137
+ pending.reject(new AdapterSchedulerError(code, 'Adapter lease request was cancelled'));
138
+ this.removeDrainedPool(pool);
139
+ return true;
140
+ }
141
+ return false;
142
+ }
143
+ sweepExpired() {
144
+ const now = this.now();
145
+ this.pruneReleasedLeaseIds(now);
146
+ for (const pool of [...this.pools.values()]) {
147
+ for (const pending of [...pool.queued]) {
148
+ if (pending.deadline > now)
149
+ continue;
150
+ pool.queued.splice(pool.queued.indexOf(pending), 1);
151
+ if (pending.timer)
152
+ clearTimeout(pending.timer);
153
+ pending.reject(new AdapterSchedulerError('ADAPTER_QUEUE_TIMEOUT', 'Timed out waiting for an Adapter command lease'));
154
+ }
155
+ for (const lease of [...pool.running.values()]) {
156
+ if (lease.heartbeatDeadline > now)
157
+ continue;
158
+ this.releaseAllResourcesForLease(lease.leaseId);
159
+ this.rejectResourceWaitersForLease(lease.leaseId);
160
+ pool.running.delete(lease.leaseId);
161
+ pool.activeSessions.delete(lease.adapterSession);
162
+ }
163
+ if (!pool.closed)
164
+ this.schedule(pool);
165
+ this.removeDrainedPool(pool);
166
+ }
167
+ for (const pending of [...this.resourceQueue]) {
168
+ if (pending.deadline > now)
169
+ continue;
170
+ this.resourceQueue.splice(this.resourceQueue.indexOf(pending), 1);
171
+ if (pending.timer)
172
+ clearTimeout(pending.timer);
173
+ pending.reject(new AdapterSchedulerError('ADAPTER_RESOURCE_TIMEOUT', 'Timed out waiting for Adapter resource locks'));
174
+ }
175
+ this.scheduleResources();
176
+ }
177
+ reset() {
178
+ for (const pool of this.pools.values()) {
179
+ for (const pending of pool.queued) {
180
+ if (pending.timer)
181
+ clearTimeout(pending.timer);
182
+ pending.reject(new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Adapter scheduler restarted'));
183
+ }
184
+ }
185
+ this.pools.clear();
186
+ for (const pending of this.resourceQueue.splice(0)) {
187
+ if (pending.timer)
188
+ clearTimeout(pending.timer);
189
+ pending.reject(new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Adapter scheduler restarted'));
190
+ }
191
+ this.resourceOwners.clear();
192
+ this.resourceGrants.clear();
193
+ this.releasedLeaseIds.clear();
194
+ }
195
+ snapshot() {
196
+ let running = 0;
197
+ let queued = 0;
198
+ for (const pool of this.pools.values()) {
199
+ running += pool.running.size;
200
+ queued += pool.queued.length;
201
+ }
202
+ return { running, queued, pools: this.pools.size };
203
+ }
204
+ resourceSnapshot() {
205
+ return {
206
+ locked: this.resourceOwners.size,
207
+ queued: this.resourceQueue.length,
208
+ grants: this.resourceGrants.size,
209
+ };
210
+ }
211
+ schedule(pool) {
212
+ while (!pool.closed && pool.running.size < pool.maxParallel) {
213
+ const eligible = pool.queued
214
+ .filter(entry => !pool.activeSessions.has(entry.request.adapterSession)
215
+ && pool.running.size < pool.maxParallel)
216
+ .sort((a, b) => a.enqueuedAt - b.enqueuedAt || a.sequence - b.sequence)[0];
217
+ if (!eligible)
218
+ return;
219
+ pool.queued.splice(pool.queued.indexOf(eligible), 1);
220
+ if (eligible.timer)
221
+ clearTimeout(eligible.timer);
222
+ const grantedAt = this.now();
223
+ const lease = {
224
+ leaseId: crypto.randomUUID(),
225
+ requestId: eligible.request.requestId,
226
+ poolKey: pool.key,
227
+ contextId: eligible.request.contextId,
228
+ surface: 'adapter',
229
+ site: eligible.request.site,
230
+ adapterSession: eligible.request.adapterSession,
231
+ sessionKey: eligible.request.sessionKey,
232
+ generation: pool.generation,
233
+ grantedAt,
234
+ heartbeatDeadline: grantedAt + this.leaseExpiryMs,
235
+ };
236
+ pool.running.set(lease.leaseId, lease);
237
+ pool.activeSessions.add(lease.adapterSession);
238
+ eligible.resolve({ ...lease });
239
+ }
240
+ }
241
+ scheduleResources() {
242
+ for (const pending of [...this.resourceQueue].sort((a, b) => a.sequence - b.sequence)) {
243
+ try {
244
+ this.requireLease(pending.lease);
245
+ }
246
+ catch {
247
+ this.resourceQueue.splice(this.resourceQueue.indexOf(pending), 1);
248
+ if (pending.timer)
249
+ clearTimeout(pending.timer);
250
+ pending.reject(new AdapterSchedulerError('ADAPTER_LEASE_LOST', 'Adapter command lease was lost while waiting for resources'));
251
+ continue;
252
+ }
253
+ if (!pending.keys.every(key => !this.resourceOwners.has(key)))
254
+ continue;
255
+ this.resourceQueue.splice(this.resourceQueue.indexOf(pending), 1);
256
+ if (pending.timer)
257
+ clearTimeout(pending.timer);
258
+ const grant = {
259
+ grantId: crypto.randomUUID(),
260
+ leaseId: pending.lease.leaseId,
261
+ keys: pending.keys,
262
+ };
263
+ this.resourceGrants.set(grant.grantId, grant);
264
+ for (const key of grant.keys)
265
+ this.resourceOwners.set(key, { leaseId: grant.leaseId, grantId: grant.grantId });
266
+ pending.resolve({ ...grant, keys: [...grant.keys] });
267
+ }
268
+ }
269
+ releaseAllResourcesForLease(leaseId) {
270
+ for (const grant of [...this.resourceGrants.values()]) {
271
+ if (grant.leaseId !== leaseId)
272
+ continue;
273
+ this.resourceGrants.delete(grant.grantId);
274
+ for (const key of grant.keys) {
275
+ const owner = this.resourceOwners.get(key);
276
+ if (owner?.grantId === grant.grantId)
277
+ this.resourceOwners.delete(key);
278
+ }
279
+ }
280
+ }
281
+ pruneReleasedLeaseIds(now = this.now()) {
282
+ for (const [leaseId, expiresAt] of this.releasedLeaseIds) {
283
+ if (expiresAt <= now)
284
+ this.releasedLeaseIds.delete(leaseId);
285
+ }
286
+ }
287
+ rejectResourceWaitersForLease(leaseId) {
288
+ for (const pending of [...this.resourceQueue]) {
289
+ if (pending.lease.leaseId !== leaseId)
290
+ continue;
291
+ this.resourceQueue.splice(this.resourceQueue.indexOf(pending), 1);
292
+ if (pending.timer)
293
+ clearTimeout(pending.timer);
294
+ pending.reject(new AdapterSchedulerError('ADAPTER_LEASE_LOST', 'Adapter command lease ended while waiting for resources'));
295
+ }
296
+ }
297
+ requireLease(identity) {
298
+ const pool = this.pools.get(identity.poolKey);
299
+ const lease = pool?.running.get(identity.leaseId);
300
+ if (!lease
301
+ || lease.requestId !== identity.requestId
302
+ || lease.contextId !== identity.contextId
303
+ || lease.site !== identity.site
304
+ || lease.adapterSession !== identity.adapterSession
305
+ || lease.sessionKey !== identity.sessionKey
306
+ || lease.generation !== identity.generation) {
307
+ throw new AdapterSchedulerError('ADAPTER_LEASE_LOST', 'Adapter command lease is no longer valid');
308
+ }
309
+ return lease;
310
+ }
311
+ getOrCreatePool(request) {
312
+ const key = `${request.contextId}\u0000${request.surface}\u0000${request.site}`;
313
+ const existing = this.pools.get(key);
314
+ if (existing)
315
+ return existing;
316
+ const generation = (this.generations.get(key) ?? 0) + 1;
317
+ this.generations.set(key, generation);
318
+ const pool = {
319
+ key,
320
+ generation,
321
+ maxParallel: Math.min(request.maxParallel, this.runtimeCeiling),
322
+ running: new Map(),
323
+ activeSessions: new Set(),
324
+ queued: [],
325
+ };
326
+ this.pools.set(key, pool);
327
+ return pool;
328
+ }
329
+ removeDrainedPool(pool) {
330
+ if (pool.running.size === 0 && pool.queued.length === 0)
331
+ this.pools.delete(pool.key);
332
+ }
333
+ poolClosedError(reason) {
334
+ return reason === 'auth_gate'
335
+ ? new AdapterSchedulerError('ADAPTER_POOL_AUTH_GATE', 'Adapter pool stopped at an authentication or verification gate')
336
+ : new AdapterSchedulerError('ADAPTER_POOL_RATE_LIMITED', 'Adapter pool stopped after account rate limiting');
337
+ }
338
+ validateRequest(request) {
339
+ if (!request.requestId || !request.contextId || !request.site || !request.adapterSession || !request.sessionKey) {
340
+ throw new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Invalid Adapter lease request');
341
+ }
342
+ if (!Number.isInteger(request.queueTimeoutMs) || request.queueTimeoutMs < 1) {
343
+ throw new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Invalid Adapter queue timeout');
344
+ }
345
+ if (!Number.isInteger(request.maxParallel) || request.maxParallel < 1 || request.maxParallel > this.runtimeCeiling) {
346
+ throw new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Invalid Adapter concurrency limit');
347
+ }
348
+ }
349
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Provides a typed send() function that posts a Command and returns a Result.
5
5
  */
6
+ import type { AdapterLease, AdapterLeaseRelease, AdapterLeaseRequest, AdapterResourceGrant } from '../adapter-scheduler.js';
6
7
  export interface DaemonCommand {
7
8
  id: string;
8
9
  action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'ima-auth-start' | 'ima-auth-read' | 'ima-reader-request' | 'ima-auth-release' | 'wait-download' | 'cdp' | 'frames';
@@ -47,6 +48,8 @@ export interface DaemonCommand {
47
48
  frameIndex?: number;
48
49
  /** Browser profile/context to route the command to. */
49
50
  contextId?: string;
51
+ /** Active Adapter lease used by the daemon to fence every browser action. */
52
+ adapterLease?: AdapterLease;
50
53
  }
51
54
  export interface DaemonResult {
52
55
  id: string;
@@ -107,6 +110,12 @@ export type DaemonStatusProbe = {
107
110
  } | {
108
111
  kind: 'network_error';
109
112
  };
113
+ export declare function acquireAdapterLease(request: AdapterLeaseRequest): Promise<AdapterLease>;
114
+ export declare function heartbeatAdapterLease(lease: AdapterLease): Promise<AdapterLease>;
115
+ export declare function releaseAdapterLease(release: AdapterLeaseRelease): Promise<boolean>;
116
+ export declare function cancelAdapterLease(requestId: string): Promise<boolean>;
117
+ export declare function acquireAdapterResources(lease: AdapterLease, keys: string[], timeoutMs: number): Promise<AdapterResourceGrant>;
118
+ export declare function releaseAdapterResources(lease: AdapterLease, grantId: string): Promise<boolean>;
110
119
  export declare function probeDaemonStatus(opts?: {
111
120
  timeout?: number;
112
121
  contextId?: string;
@@ -139,6 +148,8 @@ export declare function getDaemonHealth(opts?: {
139
148
  timeout?: number;
140
149
  contextId?: string;
141
150
  }): Promise<DaemonHealth>;
151
+ /** Resolve the concrete daemon profile used to key Adapter scheduler pools. */
152
+ export declare function resolveAdapterLeaseContextId(requestedContextId?: string): Promise<string>;
142
153
  export declare function requestDaemonShutdown(opts?: {
143
154
  timeout?: number;
144
155
  }): Promise<boolean>;
@@ -7,6 +7,8 @@ import { sleep } from '../utils.js';
7
7
  import { resolveDaemonPort } from './daemon-config.js';
8
8
  import { classifyBrowserError } from './errors.js';
9
9
  import { resolveProfileContextId } from './profile.js';
10
+ import { AdapterCoordinationError } from '../errors.js';
11
+ import { getAdapterExecutionContext } from '../adapter-execution-context.js';
10
12
  const BYCLI_HEADERS = { 'X-byCLI': '1' };
11
13
  let _idCounter = 0;
12
14
  function generateId() {
@@ -44,6 +46,40 @@ async function consumeDaemonResponse(pathname, init, consume, port) {
44
46
  async function requestDaemon(pathname, init) {
45
47
  return consumeDaemonResponse(pathname, init, async (response) => response);
46
48
  }
49
+ async function postAdapterLease(pathname, body, timeout) {
50
+ const response = await requestDaemon(pathname, {
51
+ method: 'POST',
52
+ headers: { 'Content-Type': 'application/json' },
53
+ body: JSON.stringify(body),
54
+ timeout,
55
+ });
56
+ const envelope = await response.json();
57
+ if (!response.ok || envelope.ok !== true || envelope.data === undefined) {
58
+ throw new AdapterCoordinationError(envelope.errorCode ?? 'ADAPTER_QUEUE_RESET', envelope.error ?? 'Adapter scheduler request failed', true);
59
+ }
60
+ return envelope.data;
61
+ }
62
+ export function acquireAdapterLease(request) {
63
+ return postAdapterLease('/v1/adapter-leases/acquire', request, request.queueTimeoutMs + 5_000);
64
+ }
65
+ export function heartbeatAdapterLease(lease) {
66
+ return postAdapterLease('/v1/adapter-leases/heartbeat', lease, 5_000);
67
+ }
68
+ export async function releaseAdapterLease(release) {
69
+ const data = await postAdapterLease('/v1/adapter-leases/release', release, 5_000);
70
+ return data.released;
71
+ }
72
+ export async function cancelAdapterLease(requestId) {
73
+ const data = await postAdapterLease('/v1/adapter-leases/cancel', { requestId }, 5_000);
74
+ return data.cancelled;
75
+ }
76
+ export function acquireAdapterResources(lease, keys, timeoutMs) {
77
+ return postAdapterLease('/v1/adapter-resources/acquire', { lease, keys, timeoutMs }, timeoutMs + 5_000);
78
+ }
79
+ export async function releaseAdapterResources(lease, grantId) {
80
+ const data = await postAdapterLease('/v1/adapter-resources/release', { lease, grantId }, 5_000);
81
+ return data.released;
82
+ }
47
83
  function errorCode(error) {
48
84
  if (!error || typeof error !== 'object')
49
85
  return undefined;
@@ -102,6 +138,16 @@ export async function getDaemonHealth(opts) {
102
138
  return { state: 'no-extension', status };
103
139
  return { state: 'ready', status };
104
140
  }
141
+ /** Resolve the concrete daemon profile used to key Adapter scheduler pools. */
142
+ export async function resolveAdapterLeaseContextId(requestedContextId) {
143
+ const health = await getDaemonHealth({ contextId: requestedContextId });
144
+ if (health.state === 'ready' && health.status.contextId) {
145
+ return health.status.contextId;
146
+ }
147
+ throw new AdapterCoordinationError('ADAPTER_PROFILE_UNAVAILABLE', 'The browser daemon could not identify the authenticated profile for this Adapter session.', true, requestedContextId
148
+ ? `Check that profile "${requestedContextId}" is connected.`
149
+ : 'Connect exactly one browser profile or pass --profile explicitly.');
150
+ }
105
151
  export async function requestDaemonShutdown(opts) {
106
152
  try {
107
153
  const res = await requestDaemon('/shutdown', { method: 'POST', timeout: opts?.timeout ?? 5000 });
@@ -130,7 +176,13 @@ async function sendCommandRaw(action, params) {
130
176
  : undefined;
131
177
  const contextId = params.contextId ?? resolveProfileContextId();
132
178
  const windowMode = params.windowMode ?? envWindowMode;
133
- const command = { id, action, ...params, ...(contextId && { contextId }), ...(windowMode && { windowMode }) };
179
+ const adapterLease = getAdapterExecutionContext()?.lease;
180
+ const command = {
181
+ id, action, ...params,
182
+ ...(contextId && { contextId }),
183
+ ...(windowMode && { windowMode }),
184
+ ...(adapterLease && { adapterLease }),
185
+ };
134
186
  try {
135
187
  const res = await requestDaemon('/command', {
136
188
  method: 'POST',
@@ -1,4 +1,5 @@
1
1
  export declare const FOCUS_WINDOW_CAPABILITY = "focus-window-v1";
2
+ export declare const IMA_READER_CAPABILITY = "ima-reader-v1";
2
3
  export declare const EXTENSION_CAPABILITY_MISSING_ERROR_CODE = "extension_capability_missing";
3
4
  export declare const EXTENSION_CAPABILITY_MISSING_HTTP_STATUS = 412;
4
5
  export declare function normalizeExtensionCapabilities(value: unknown): string[];
@@ -1,14 +1,23 @@
1
1
  export const FOCUS_WINDOW_CAPABILITY = 'focus-window-v1';
2
+ export const IMA_READER_CAPABILITY = 'ima-reader-v1';
2
3
  export const EXTENSION_CAPABILITY_MISSING_ERROR_CODE = 'extension_capability_missing';
3
4
  export const EXTENSION_CAPABILITY_MISSING_HTTP_STATUS = 412;
5
+ const IMA_READER_ACTIONS = new Set([
6
+ 'ima-auth-start',
7
+ 'ima-auth-read',
8
+ 'ima-reader-request',
9
+ 'ima-auth-release',
10
+ ]);
4
11
  export function normalizeExtensionCapabilities(value) {
5
12
  if (!Array.isArray(value))
6
13
  return [];
7
14
  return [...new Set(value.filter((entry) => typeof entry === 'string' && entry.length > 0))];
8
15
  }
9
16
  export function requiredExtensionCapability(command) {
10
- return command.action === 'tabs' && command.op === 'focus'
11
- ? FOCUS_WINDOW_CAPABILITY
17
+ if (command.action === 'tabs' && command.op === 'focus')
18
+ return FOCUS_WINDOW_CAPABILITY;
19
+ return typeof command.action === 'string' && IMA_READER_ACTIONS.has(command.action)
20
+ ? IMA_READER_CAPABILITY
12
21
  : undefined;
13
22
  }
14
23
  export function missingRequiredExtensionCapability(command, capabilities) {
@@ -16,7 +25,11 @@ export function missingRequiredExtensionCapability(command, capabilities) {
16
25
  return required && !capabilities.includes(required) ? required : undefined;
17
26
  }
18
27
  export function extensionCapabilityHint(capability) {
19
- return capability === FOCUS_WINDOW_CAPABILITY
20
- ? 'Update and reload the byCLI Browser Bridge extension, then retry the login flow.'
21
- : 'Update and reload the byCLI Browser Bridge extension, then retry.';
28
+ if (capability === FOCUS_WINDOW_CAPABILITY) {
29
+ return 'Update and reload the byCLI Browser Bridge extension, then retry the login flow.';
30
+ }
31
+ if (capability === IMA_READER_CAPABILITY) {
32
+ return 'Update and reload the byCLI Browser Bridge extension with private ima reader support, then retry.';
33
+ }
34
+ return 'Update and reload the byCLI Browser Bridge extension, then retry.';
22
35
  }
@@ -15,12 +15,13 @@ import { BasePage } from './base-page.js';
15
15
  */
16
16
  export declare class Page extends BasePage {
17
17
  private readonly session;
18
- readonly contextId?: string | undefined;
18
+ contextId?: string | undefined;
19
19
  private readonly windowMode?;
20
20
  private readonly surface;
21
21
  private readonly siteSession?;
22
22
  private readonly _idleTimeout;
23
23
  constructor(session: string, idleTimeout?: number, contextId?: string | undefined, windowMode?: "foreground" | "background" | undefined, surface?: 'browser' | 'adapter', siteSession?: "ephemeral" | "persistent" | undefined);
24
+ setContextId(contextId: string): void;
24
25
  /** Active page identity (targetId), set after navigate and used in all subsequent commands */
25
26
  private _page;
26
27
  private _networkCaptureUnsupported;
@@ -51,6 +51,9 @@ export class Page extends BasePage {
51
51
  this.siteSession = siteSession;
52
52
  this._idleTimeout = idleTimeout;
53
53
  }
54
+ setContextId(contextId) {
55
+ this.contextId = contextId;
56
+ }
54
57
  /** Active page identity (targetId), set after navigate and used in all subsequent commands */
55
58
  _page;
56
59
  _networkCaptureUnsupported = false;
@@ -105,6 +105,7 @@ function toManifestEntry(cmd, modulePath, sourceFile) {
105
105
  sourceFile,
106
106
  navigateBefore: cmd.navigateBefore,
107
107
  siteSession: cmd.siteSession,
108
+ adapterConcurrency: cmd.adapterConcurrency,
108
109
  };
109
110
  }
110
111
  /**
@@ -50,6 +50,9 @@ export interface DashPositionalManifestEntry {
50
50
  default?: unknown;
51
51
  }>;
52
52
  browser?: boolean;
53
+ adapterConcurrency?: {
54
+ isolatedTabs?: boolean;
55
+ };
53
56
  }
54
57
  /**
55
58
  * `bycli boss detail -abc123def` fails because commander parses
@@ -144,6 +144,10 @@ function knownCommandOptions(cmd) {
144
144
  options.set('--site-session', 'required');
145
145
  options.set('--keep-tab', 'required');
146
146
  }
147
+ if (cmd.adapterConcurrency?.isolatedTabs === true) {
148
+ options.set('--adapter-session', 'required');
149
+ options.set('--adapter-queue-timeout', 'required');
150
+ }
147
151
  for (const arg of cmd.args ?? []) {
148
152
  if (arg.positional)
149
153
  continue;
@@ -54,6 +54,11 @@ export function registerCommandToProgram(siteCmd, cmd) {
54
54
  .option('--site-session <mode>', 'Adapter site session lifecycle: ephemeral or persistent')
55
55
  .option('--keep-tab <bool>', 'Keep the browser tab lease after the command finishes');
56
56
  }
57
+ if (cmd.adapterConcurrency?.isolatedTabs === true) {
58
+ subCmd
59
+ .option('--adapter-session <name>', 'Named persistent Adapter tab session')
60
+ .option('--adapter-queue-timeout <seconds>', 'Seconds to wait for an Adapter command lease');
61
+ }
57
62
  const originalHelpInformation = subCmd.helpInformation.bind(subCmd);
58
63
  subCmd.helpInformation = ((contextOptions) => {
59
64
  const format = getRequestedHelpFormat();
@@ -111,6 +116,12 @@ export function registerCommandToProgram(siteCmd, cmd) {
111
116
  ...(hasBrowserCapability(cmd) && typeof optionsRecord.window === 'string' ? { windowMode: optionsRecord.window } : {}),
112
117
  ...(hasBrowserCapability(cmd) && typeof optionsRecord.siteSession === 'string' ? { siteSession: optionsRecord.siteSession } : {}),
113
118
  ...(hasBrowserCapability(cmd) && typeof optionsRecord.keepTab === 'string' ? { keepTab: optionsRecord.keepTab } : {}),
119
+ ...(cmd.adapterConcurrency?.isolatedTabs === true && typeof optionsRecord.adapterSession === 'string'
120
+ ? { adapterSession: optionsRecord.adapterSession }
121
+ : {}),
122
+ ...(cmd.adapterConcurrency?.isolatedTabs === true && typeof optionsRecord.adapterQueueTimeout === 'string'
123
+ ? { adapterQueueTimeout: optionsRecord.adapterQueueTimeout }
124
+ : {}),
114
125
  });
115
126
  if (result === null || result === undefined) {
116
127
  return;