@parall/daemon 1.32.1 → 1.34.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 (46) hide show
  1. package/bundle/bb-browser-daemon.js +15628 -0
  2. package/bundle/buildDomTree.js +1501 -0
  3. package/bundle/manifest.json +19 -11
  4. package/bundle/parall-claude-agent.js +185 -25
  5. package/bundle/parall-codex-agent.js +185 -25
  6. package/bundle/parall-daemon.js +4647 -2856
  7. package/bundle/parall-openclaw-agent.js +4 -3
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +8 -2
  10. package/dist/clip-runtime/browser-dependency.d.ts +20 -0
  11. package/dist/clip-runtime/browser-dependency.d.ts.map +1 -0
  12. package/dist/clip-runtime/browser-dependency.js +52 -0
  13. package/dist/clip-runtime/browser-profile-manager.d.ts +70 -0
  14. package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -0
  15. package/dist/clip-runtime/browser-profile-manager.js +608 -0
  16. package/dist/clip-runtime/clip-provider.d.ts +10 -1
  17. package/dist/clip-runtime/clip-provider.d.ts.map +1 -1
  18. package/dist/clip-runtime/clip-provider.js +63 -21
  19. package/dist/clip-runtime/hub-client.d.ts +79 -0
  20. package/dist/clip-runtime/hub-client.d.ts.map +1 -0
  21. package/dist/clip-runtime/hub-client.js +320 -0
  22. package/dist/clip-runtime/index.d.ts +2 -0
  23. package/dist/clip-runtime/index.d.ts.map +1 -1
  24. package/dist/clip-runtime/index.js +2 -0
  25. package/dist/clip-runtime/ipc.d.ts +6 -0
  26. package/dist/clip-runtime/ipc.d.ts.map +1 -1
  27. package/dist/clip-runtime/manifest.d.ts +16 -8
  28. package/dist/clip-runtime/manifest.d.ts.map +1 -1
  29. package/dist/clip-runtime/manifest.js +13 -0
  30. package/dist/clip-runtime/process-manager.d.ts +55 -3
  31. package/dist/clip-runtime/process-manager.d.ts.map +1 -1
  32. package/dist/clip-runtime/process-manager.js +226 -48
  33. package/dist/clip-runtime/process.d.ts +15 -1
  34. package/dist/clip-runtime/process.d.ts.map +1 -1
  35. package/dist/clip-runtime/process.js +73 -10
  36. package/dist/config.d.ts +9 -0
  37. package/dist/config.d.ts.map +1 -1
  38. package/dist/config.js +12 -0
  39. package/dist/index.js +46 -5
  40. package/dist/runtime-bin-resolver.d.ts +7 -0
  41. package/dist/runtime-bin-resolver.d.ts.map +1 -0
  42. package/dist/runtime-bin-resolver.js +292 -0
  43. package/dist/supervisor.d.ts +53 -4
  44. package/dist/supervisor.d.ts.map +1 -1
  45. package/dist/supervisor.js +449 -117
  46. package/package.json +7 -6
@@ -0,0 +1,608 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import * as net from 'node:net';
6
+ import * as path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ const BB_BROWSER_DAEMON_START_TIMEOUT_MS = 15_000;
9
+ class BrowserCommandError extends Error {
10
+ method;
11
+ account;
12
+ constructor(message, method, account) {
13
+ super(message);
14
+ this.method = method;
15
+ this.account = account;
16
+ this.name = 'BrowserCommandError';
17
+ }
18
+ }
19
+ export class BrowserProfileManager {
20
+ opts;
21
+ daemon = null;
22
+ starting = null;
23
+ restarting = null;
24
+ ensuredAccounts = new Set();
25
+ ensuringAccounts = new Map();
26
+ reportedStatuses = new Map();
27
+ constructor(opts) {
28
+ this.opts = opts;
29
+ mkdirSync(this.opts.homeDir, { recursive: true });
30
+ }
31
+ async invoke({ profileId, account, command, input }) {
32
+ if (!profileId)
33
+ throw new Error('browser profile id is required');
34
+ if (!account)
35
+ throw new Error('browser account is required');
36
+ try {
37
+ await this.withDaemonRecovery(() => this.prepareInvokeAccount(account), `prepare invoke ${command} for ${profileId}`);
38
+ }
39
+ catch (err) {
40
+ const message = err instanceof Error ? err.message : String(err);
41
+ this.reportStatus(profileId, 'error', message);
42
+ throw err;
43
+ }
44
+ let request;
45
+ try {
46
+ request = await this.buildCommandRequest(account, command, input);
47
+ }
48
+ catch (err) {
49
+ if (isRecoverableBrowserDaemonError(err)) {
50
+ await this.recoverInvokeAvailability(`build invoke ${command} for ${profileId}`, profileId, err);
51
+ }
52
+ throw err;
53
+ }
54
+ try {
55
+ // Do not replay arbitrary forwarded browser commands: click/type/eval can
56
+ // have user-visible side effects if Chrome consumed the first attempt.
57
+ const result = await this.sendCommand(request);
58
+ this.reportStatus(profileId, 'running');
59
+ return result;
60
+ }
61
+ catch (err) {
62
+ if (isRecoverableBrowserDaemonError(err)) {
63
+ await this.recoverInvokeAvailability(`invoke ${command} for ${profileId}`, profileId, err);
64
+ }
65
+ throw err;
66
+ }
67
+ }
68
+ async ensureRuntime(profileId) {
69
+ if (!profileId)
70
+ throw new Error('browser profile id is required');
71
+ try {
72
+ await this.withDaemonRecovery(() => this.ensureAccount(profileId), `ensure runtime for ${profileId}`);
73
+ this.reportStatus(profileId, 'running');
74
+ }
75
+ catch (err) {
76
+ const message = err instanceof Error ? err.message : String(err);
77
+ this.reportStatus(profileId, 'error', message);
78
+ throw err;
79
+ }
80
+ }
81
+ async openProfile(profileId, startUrl) {
82
+ if (!profileId)
83
+ throw new Error('browser profile id is required');
84
+ // Explicit lifecycle actions must always report their outcome: the server
85
+ // resets the profile to `pending` on the API call, so a deduped repeat of
86
+ // our previous report (same status+message marker) would leave the profile
87
+ // stuck in pending forever. Clearing the marker keeps the dedup for the
88
+ // chatty invoke path while making lifecycle outcomes authoritative.
89
+ this.reportedStatuses.delete(profileId);
90
+ const url = normalizeStartUrl(startUrl);
91
+ try {
92
+ await this.withDaemonRecovery(async () => {
93
+ const prepareResult = await this.prepareOpenProfile(profileId, url);
94
+ await this.openProfileTabOnce(profileId, url, prepareResult.accountCreated);
95
+ }, `open profile ${profileId}`);
96
+ this.reportStatus(profileId, 'running');
97
+ }
98
+ catch (err) {
99
+ const message = err instanceof Error ? err.message : String(err);
100
+ this.reportStatus(profileId, 'error', message);
101
+ throw err;
102
+ }
103
+ }
104
+ async stopProfile(profileId) {
105
+ if (!profileId)
106
+ throw new Error('browser profile id is required');
107
+ this.reportedStatuses.delete(profileId); // see openProfile — lifecycle outcomes always report
108
+ // bb-browser-pro does not expose a single-account "dispose context" command.
109
+ // Server-side stopped status is the authority that gates future invokes.
110
+ this.reportStatus(profileId, 'stopped');
111
+ }
112
+ async resetProfile(profileId) {
113
+ if (!profileId)
114
+ throw new Error('browser profile id is required');
115
+ this.reportedStatuses.delete(profileId); // see openProfile — lifecycle outcomes always report
116
+ try {
117
+ await this.withDaemonRecovery(() => this.resetProfileOnce(profileId), `reset profile ${profileId}`);
118
+ this.reportStatus(profileId, 'stopped');
119
+ }
120
+ catch (err) {
121
+ const message = err instanceof Error ? err.message : String(err);
122
+ this.reportStatus(profileId, 'error', message);
123
+ throw err;
124
+ }
125
+ }
126
+ async stop() {
127
+ const daemon = this.daemon;
128
+ this.daemon = null;
129
+ this.starting = null;
130
+ this.ensuredAccounts.clear();
131
+ this.ensuringAccounts.clear();
132
+ this.reportedStatuses.clear();
133
+ if (!daemon)
134
+ return;
135
+ try {
136
+ await this.post('/shutdown', daemon, undefined, 3_000);
137
+ }
138
+ catch {
139
+ /* best-effort */
140
+ }
141
+ if (daemon.child.exitCode === null && daemon.child.signalCode === null) {
142
+ daemon.child.kill('SIGTERM');
143
+ }
144
+ }
145
+ async prepareOpenProfile(profileId, url) {
146
+ const accountCreated = await this.ensureAccount(profileId, url !== 'about:blank' ? url : undefined);
147
+ await this.sendCommand({ method: 'tab_list', account: profileId });
148
+ return { accountCreated };
149
+ }
150
+ async openProfileTabOnce(profileId, url, accountCreated) {
151
+ if (accountCreated)
152
+ return;
153
+ if (await this.profileAlreadyHasOpenTab(profileId, url))
154
+ return;
155
+ await this.sendCommand({ method: 'tab_new', account: profileId, url });
156
+ }
157
+ async resetProfileOnce(profileId) {
158
+ if (await this.accountExists(profileId)) {
159
+ await this.sendCommand({ method: 'account_delete', account: profileId });
160
+ }
161
+ this.ensuredAccounts.delete(profileId);
162
+ }
163
+ async prepareInvokeAccount(account) {
164
+ await this.ensureAccount(account);
165
+ await this.sendCommand({ method: 'tab_list', account });
166
+ }
167
+ async recoverInvokeAvailability(label, profileId, err) {
168
+ try {
169
+ await this.restartDaemon(label, err);
170
+ this.reportStatus(profileId, 'running');
171
+ }
172
+ catch (restartErr) {
173
+ const message = restartErr instanceof Error ? restartErr.message : String(restartErr);
174
+ this.reportStatus(profileId, 'error', message);
175
+ throw restartErr;
176
+ }
177
+ }
178
+ async accountExists(account) {
179
+ try {
180
+ await this.sendCommand({ method: 'account_info', account });
181
+ return true;
182
+ }
183
+ catch (err) {
184
+ if (isBrowserAccountInfoUnauthenticatedError(err))
185
+ return true;
186
+ if (isBrowserAccountNotFoundError(err))
187
+ return false;
188
+ throw err;
189
+ }
190
+ }
191
+ async buildCommandRequest(account, command, input) {
192
+ const body = input && typeof input === 'object' && !Array.isArray(input)
193
+ ? { ...input }
194
+ : { value: input };
195
+ const request = body;
196
+ request.method = command;
197
+ request.account = account;
198
+ if (command === 'eval' && typeof request.domain === 'string' && request.tabId === undefined) {
199
+ const tabRef = await this.resolveAccountDomainTab(account, request.domain);
200
+ if (tabRef !== undefined)
201
+ request.tabId = tabRef;
202
+ }
203
+ return request;
204
+ }
205
+ /**
206
+ * Account-scoped version of bb-browser's `resolveTabByDomain` (which is
207
+ * account-blind — the reason for this daemon-side preselection workaround).
208
+ * Two properties the bare `tab_new` + eval approach lacked, both caught on
209
+ * the staging closed-loop E2E (2026-06-04):
210
+ *
211
+ * 1. Reuse: an existing account tab already on the domain is reused instead
212
+ * of opening a new tab per eval (upstream reuses matching tabs too).
213
+ * 2. Load wait: after creating a tab, wait for the navigation to commit
214
+ * before eval — upstream waits (~10s poll + settle); without it the clip
215
+ * script races `about:blank` and relative fetches fail
216
+ * ("Failed to parse URL from /hot.json").
217
+ */
218
+ async resolveAccountDomainTab(account, domain) {
219
+ const url = normalizeDomainUrl(domain);
220
+ let host = '';
221
+ try {
222
+ host = new URL(url).host;
223
+ }
224
+ catch {
225
+ /* fall through — tab_new with whatever bb-browser makes of it */
226
+ }
227
+ if (host) {
228
+ const existing = await this.findAccountTabOnHost(account, host);
229
+ if (existing !== undefined)
230
+ return existing;
231
+ }
232
+ const tab = await this.sendCommand({ method: 'tab_new', url, account });
233
+ const tabRef = (tab?.tab ?? tab?.tabId);
234
+ if (tabRef === undefined || !host)
235
+ return tabRef;
236
+ // Wait for the created tab to actually reach the domain (navigation
237
+ // committed). Mirrors upstream resolveTabByDomain's bounded wait + settle.
238
+ const deadline = Date.now() + 10_000;
239
+ while (Date.now() < deadline) {
240
+ const found = await this.findAccountTabOnHost(account, host, tabRef);
241
+ if (found !== undefined)
242
+ break;
243
+ await sleep(300);
244
+ }
245
+ await sleep(750); // post-commit settle (upstream uses a flat 2s)
246
+ return tabRef;
247
+ }
248
+ /** Find an account-owned tab whose URL host matches (optionally a specific tab). */
249
+ async findAccountTabOnHost(account, host, onlyTabRef) {
250
+ const list = await this.sendCommand({ method: 'tab_list', account });
251
+ const tabs = Array.isArray(list.tabs)
252
+ ? list.tabs
253
+ : [];
254
+ for (const t of tabs) {
255
+ if (t?.account !== account)
256
+ continue;
257
+ const ref = (t.tab ?? t.tabId);
258
+ if (onlyTabRef !== undefined && ref !== onlyTabRef && t.tabId !== onlyTabRef)
259
+ continue;
260
+ if (typeof t.url !== 'string')
261
+ continue;
262
+ try {
263
+ if (hostsMatch(new URL(t.url).host, host))
264
+ return ref;
265
+ }
266
+ catch {
267
+ /* non-URL tab (about:blank etc.) — skip */
268
+ }
269
+ }
270
+ return undefined;
271
+ }
272
+ async ensureAccount(account, accountUrl) {
273
+ if (this.ensuredAccounts.has(account))
274
+ return false;
275
+ const inflight = this.ensuringAccounts.get(account);
276
+ if (inflight)
277
+ return inflight;
278
+ const ensure = (async () => {
279
+ let accountCreated = false;
280
+ try {
281
+ await this.sendCommand({ method: 'account_info', account });
282
+ }
283
+ catch (err) {
284
+ if (!isBrowserAccountInfoUnauthenticatedError(err)) {
285
+ if (!isBrowserAccountNotFoundError(err))
286
+ throw err;
287
+ await this.sendCommand({
288
+ method: 'account_create',
289
+ account,
290
+ ...(accountUrl ? { accountUrl } : {}),
291
+ });
292
+ accountCreated = true;
293
+ }
294
+ }
295
+ if (!accountUrl) {
296
+ await this.ensureAccountOwnedTab(account);
297
+ }
298
+ this.ensuredAccounts.add(account);
299
+ return accountCreated;
300
+ })();
301
+ this.ensuringAccounts.set(account, ensure);
302
+ try {
303
+ return await ensure;
304
+ }
305
+ finally {
306
+ this.ensuringAccounts.delete(account);
307
+ }
308
+ }
309
+ async ensureAccountOwnedTab(account) {
310
+ if (await this.hasAccountOwnedTab(account))
311
+ return;
312
+ await this.sendCommand({ method: 'tab_new', account, url: 'about:blank' });
313
+ }
314
+ async profileAlreadyHasOpenTab(profileId, url) {
315
+ // Lifecycle `open` can be delivered more than once (WS + pending reconcile,
316
+ // retry, or duplicate user action). Treat only the requested target URL as
317
+ // already-open; a same-host different path is still a distinct user intent.
318
+ if (url === 'about:blank')
319
+ return this.hasAccountOwnedTab(profileId);
320
+ return this.findAccountTabMatchingUrl(profileId, url);
321
+ }
322
+ async hasAccountOwnedTab(account) {
323
+ const list = await this.sendCommand({ method: 'tab_list', account });
324
+ const tabs = Array.isArray(list.tabs)
325
+ ? list.tabs
326
+ : [];
327
+ return tabs.some((tab) => tab?.account === account);
328
+ }
329
+ async findAccountTabMatchingUrl(account, url) {
330
+ const expected = comparableUrl(url);
331
+ if (!expected)
332
+ return false;
333
+ const list = await this.sendCommand({ method: 'tab_list', account });
334
+ const tabs = Array.isArray(list.tabs)
335
+ ? list.tabs
336
+ : [];
337
+ for (const tab of tabs) {
338
+ if (tab?.account !== account || typeof tab.url !== 'string')
339
+ continue;
340
+ const actual = comparableUrl(tab.url);
341
+ if (!actual)
342
+ continue;
343
+ const sameResource = hostsMatch(actual.host, expected.host) && actual.target === expected.target;
344
+ const sameProtocol = actual.protocol === expected.protocol;
345
+ const redirectedUpgrade = expected.protocol === 'http:' && actual.protocol === 'https:';
346
+ if (sameResource && (sameProtocol || redirectedUpgrade))
347
+ return true;
348
+ }
349
+ return false;
350
+ }
351
+ reportStatus(profileId, status, errorMsg) {
352
+ const marker = `${status}\u0000${errorMsg ?? ''}`;
353
+ if (this.reportedStatuses.get(profileId) === marker)
354
+ return;
355
+ this.reportedStatuses.set(profileId, marker);
356
+ this.opts.reportStatus?.(profileId, status, errorMsg);
357
+ }
358
+ async sendCommand(request) {
359
+ const daemon = await this.ensureDaemon();
360
+ const response = await this.post('/command', daemon, request, 30_000);
361
+ if (response.error) {
362
+ throw new BrowserCommandError(response.error.message || 'bb-browser command failed', request.method, request.account);
363
+ }
364
+ return response.result ?? {};
365
+ }
366
+ async ensureDaemon() {
367
+ if (this.restarting)
368
+ await this.restarting;
369
+ if (this.daemon &&
370
+ this.daemon.child.exitCode === null &&
371
+ this.daemon.child.signalCode === null) {
372
+ return this.daemon;
373
+ }
374
+ if (this.starting)
375
+ return this.starting;
376
+ this.starting = this.startDaemon();
377
+ try {
378
+ this.daemon = await this.starting;
379
+ return this.daemon;
380
+ }
381
+ finally {
382
+ this.starting = null;
383
+ }
384
+ }
385
+ async withDaemonRecovery(operation, label) {
386
+ try {
387
+ return await operation();
388
+ }
389
+ catch (err) {
390
+ if (!isRecoverableBrowserDaemonError(err))
391
+ throw err;
392
+ await this.restartDaemon(label, err);
393
+ return operation();
394
+ }
395
+ }
396
+ async restartDaemon(label, err) {
397
+ if (this.restarting)
398
+ return this.restarting;
399
+ this.restarting = this.restartDaemonOnce(label, err);
400
+ try {
401
+ await this.restarting;
402
+ }
403
+ finally {
404
+ this.restarting = null;
405
+ }
406
+ }
407
+ async restartDaemonOnce(label, err) {
408
+ const daemon = this.daemon;
409
+ this.daemon = null;
410
+ this.starting = null;
411
+ this.ensuredAccounts.clear();
412
+ this.ensuringAccounts.clear();
413
+ this.opts.log.warn(`[bb-browser] ${label} hit an unavailable Chrome/CDP page target; restarting bb-browser-daemon (${formatErrorForLog(err)})`);
414
+ if (!daemon)
415
+ return;
416
+ try {
417
+ await this.post('/shutdown', daemon, undefined, 3_000);
418
+ }
419
+ catch {
420
+ /* best-effort */
421
+ }
422
+ if (daemon.child.exitCode === null && daemon.child.signalCode === null) {
423
+ daemon.child.kill('SIGTERM');
424
+ }
425
+ }
426
+ async startDaemon() {
427
+ const host = '127.0.0.1';
428
+ const port = await findFreePort();
429
+ const token = randomToken();
430
+ const daemonPath = resolveBbBrowserDaemonPath();
431
+ const child = spawn(process.execPath, [daemonPath, '--host', host, '--port', String(port), '--token', token], {
432
+ env: {
433
+ ...process.env,
434
+ BB_BROWSER_HOME: this.opts.homeDir,
435
+ },
436
+ stdio: ['ignore', 'ignore', 'pipe'],
437
+ });
438
+ // A spawn/exec failure (ENOENT, EACCES, …) emits 'error' on the child, which
439
+ // — with no listener — Node re-throws as an uncaught exception and crashes
440
+ // the daemon. Capture it so startup fails through the normal path instead.
441
+ let childError = null;
442
+ child.once('error', (err) => {
443
+ childError = err;
444
+ });
445
+ child.stderr?.on('data', (chunk) => {
446
+ const text = chunk.toString('utf8').trim();
447
+ if (text)
448
+ this.opts.log.warn(`[bb-browser] ${text}`);
449
+ });
450
+ const state = { child, host, port, token };
451
+ const deadline = Date.now() + BB_BROWSER_DAEMON_START_TIMEOUT_MS;
452
+ while (Date.now() < deadline) {
453
+ if (childError) {
454
+ throw childError;
455
+ }
456
+ if (child.exitCode !== null || child.signalCode !== null) {
457
+ throw new Error(`bb-browser-daemon exited during startup`);
458
+ }
459
+ try {
460
+ await this.post('/status', state, undefined, 2_000);
461
+ this.opts.log.info(`[bb-browser] daemon ready at ${host}:${port}`);
462
+ return state;
463
+ }
464
+ catch {
465
+ await sleep(200);
466
+ }
467
+ }
468
+ child.kill('SIGTERM');
469
+ throw new Error('bb-browser-daemon did not start in time');
470
+ }
471
+ async post(pathName, daemon, body, timeoutMs = 10_000) {
472
+ const controller = new AbortController();
473
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
474
+ try {
475
+ const resp = await fetch(`http://${daemon.host}:${daemon.port}${pathName}`, {
476
+ method: pathName === '/status' ? 'GET' : 'POST',
477
+ headers: {
478
+ Authorization: `Bearer ${daemon.token}`,
479
+ ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
480
+ },
481
+ body: body === undefined ? undefined : JSON.stringify(body),
482
+ signal: controller.signal,
483
+ });
484
+ const text = await resp.text();
485
+ if (!resp.ok) {
486
+ throw new Error(`bb-browser ${pathName} returned ${resp.status}: ${text}`);
487
+ }
488
+ return (text ? JSON.parse(text) : {});
489
+ }
490
+ finally {
491
+ clearTimeout(timer);
492
+ }
493
+ }
494
+ }
495
+ /** Host comparison tolerant of a `www.` prefix on either side. */
496
+ function hostsMatch(a, b) {
497
+ const norm = (h) => h.toLowerCase().replace(/^www\./, '');
498
+ return norm(a) === norm(b);
499
+ }
500
+ function normalizeDomainUrl(domain) {
501
+ const trimmed = domain.trim();
502
+ if (!trimmed)
503
+ return 'about:blank';
504
+ return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
505
+ }
506
+ function normalizeStartUrl(value) {
507
+ const trimmed = value?.trim();
508
+ if (!trimmed)
509
+ return 'about:blank';
510
+ if (trimmed === 'about:blank')
511
+ return trimmed;
512
+ return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
513
+ }
514
+ function isBrowserAccountNotFoundError(err) {
515
+ if (!(err instanceof BrowserCommandError) || err.method !== 'account_info')
516
+ return false;
517
+ const message = err.message.trim().toLowerCase();
518
+ if (!message)
519
+ return false;
520
+ const notFound = message === 'not found' ||
521
+ message.includes('account not found') ||
522
+ message.includes('account does not exist') ||
523
+ message.includes('unknown account') ||
524
+ message.includes('missing account') ||
525
+ // bb-browser-pro 0.15.x interpolates the account name into the message:
526
+ // `Account "<id>" not found`. Without this arm the matcher misses the
527
+ // real daemon's format entirely, so account_create never runs and every
528
+ // open of a fresh profile fails (caught on staging E2E 2026-06-04 with a
529
+ // real local daemon + Chrome).
530
+ /^account\s+"[^"]*"\s+not found\b/.test(message);
531
+ return notFound;
532
+ }
533
+ function isBrowserAccountInfoUnauthenticatedError(err) {
534
+ if (!(err instanceof BrowserCommandError) || err.method !== 'account_info')
535
+ return false;
536
+ const message = err.message.trim().toLowerCase();
537
+ return message.includes('not logged in') || message.includes('not authenticated');
538
+ }
539
+ function isRecoverableBrowserDaemonError(err) {
540
+ const message = err instanceof Error ? err.message : String(err);
541
+ const lower = message.toLowerCase();
542
+ return (lower.includes('chrome not connected') ||
543
+ lower.includes('cdp at 127.0.0.1') ||
544
+ lower.includes('no page target found') ||
545
+ (lower.includes('bb-browser /command returned 503') &&
546
+ (lower.includes('chrome') || lower.includes('cdp'))));
547
+ }
548
+ function comparableUrl(url) {
549
+ try {
550
+ const parsed = new URL(url);
551
+ return {
552
+ protocol: parsed.protocol,
553
+ host: parsed.host,
554
+ target: `${parsed.pathname}${parsed.search}${parsed.hash}`,
555
+ };
556
+ }
557
+ catch {
558
+ return null;
559
+ }
560
+ }
561
+ function formatErrorForLog(err) {
562
+ const message = err instanceof Error ? err.message : String(err);
563
+ return message.replace(/\s+/g, ' ').slice(0, 300);
564
+ }
565
+ function resolveBbBrowserDaemonPath() {
566
+ // Bundle channels (CDN self-update, desktop, npm bin — all run the flat
567
+ // esbuild artifact with NO node_modules) ship bb-browser as a sibling flat
568
+ // artifact (scripts/bundle-daemon.mjs). Prefer it; fall through to normal
569
+ // package resolution for dev/dist runs. Without this, every bundle-delivered
570
+ // daemon failed browser profiles with "Cannot find module
571
+ // '@pinixai/bb-browser-pro/package.json'" (desktop staging, 2026-06-04).
572
+ const sibling = path.join(path.dirname(fileURLToPath(import.meta.url)), 'bb-browser-daemon.js');
573
+ if (existsSync(sibling))
574
+ return sibling;
575
+ const require = createRequire(import.meta.url);
576
+ const pkgPath = require.resolve('@pinixai/bb-browser-pro/package.json');
577
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
578
+ const rel = typeof pkg.bin === 'string' ? pkg.bin : (pkg.bin?.['bb-browser-daemon'] ?? './dist/daemon.js');
579
+ const full = path.resolve(path.dirname(pkgPath), rel);
580
+ if (!existsSync(full)) {
581
+ throw new Error(`bb-browser-daemon entrypoint not found at ${full}`);
582
+ }
583
+ return full;
584
+ }
585
+ async function findFreePort() {
586
+ return new Promise((resolve, reject) => {
587
+ const server = net.createServer();
588
+ server.unref();
589
+ server.on('error', reject);
590
+ server.listen(0, '127.0.0.1', () => {
591
+ const address = server.address();
592
+ server.close(() => {
593
+ if (address && typeof address === 'object') {
594
+ resolve(address.port);
595
+ }
596
+ else {
597
+ reject(new Error('failed to allocate free port'));
598
+ }
599
+ });
600
+ });
601
+ });
602
+ }
603
+ function randomToken() {
604
+ return randomBytes(16).toString('hex');
605
+ }
606
+ function sleep(ms) {
607
+ return new Promise((resolve) => setTimeout(resolve, ms));
608
+ }
@@ -31,6 +31,14 @@ export interface ClipProviderOptions {
31
31
  warn(msg: string): void;
32
32
  error(msg: string): void;
33
33
  };
34
+ /**
35
+ * Invoked after several consecutive reconnect failures (throttled by backoff).
36
+ * Lets the supervisor refetch machine config and, if the endpoint changed,
37
+ * rebuild this provider — e.g. when clip_provider_url rolls out server-side
38
+ * after this daemon booted while its ws-gateway WS never bounced to
39
+ * re-trigger machine.hello. Optional.
40
+ */
41
+ onPersistentFailure?: () => void;
34
42
  }
35
43
  export declare class ClipProvider {
36
44
  private readonly opts;
@@ -44,10 +52,11 @@ export declare class ClipProvider {
44
52
  private statusUnsubscribe;
45
53
  private manifestUnsubscribe;
46
54
  private needsReregister;
47
- private intentionalClose;
55
+ private streamGeneration;
48
56
  private static RECONNECT_BASE_MS;
49
57
  private static RECONNECT_MAX_MS;
50
58
  private static HEARTBEAT_INTERVAL_MS;
59
+ private static PERSISTENT_FAILURE_ATTEMPT;
51
60
  constructor(opts: ClipProviderOptions);
52
61
  /** Connect to the Clip Service and register local clips. Reconnects on failure. */
53
62
  connect(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"clip-provider.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/clip-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AA0I/D,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,YAAY,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,WAAW,EAAE,kBAAkB,CAAC;IAChC,mBAAmB;IACnB,GAAG,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CACrF;AAED,qBAAa,YAAY;IAiBX,OAAO,CAAC,QAAQ,CAAC,IAAI;IAhBjC,OAAO,CAAC,OAAO,CAAyC;IACxD,OAAO,CAAC,MAAM,CAAwC;IACtD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,gBAAgB,CAAS;IAEjC,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAS;IACzC,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAU;IACzC,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAU;gBAEjB,IAAI,EAAE,mBAAmB;IAEtD,mFAAmF;IAC7E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B,wCAAwC;IAClC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAUjC,WAAW,IAAI,OAAO;YAQR,UAAU;IAoExB,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,sBAAsB;IAyB9B,OAAO,CAAC,wBAAwB;IAWhC,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,gBAAgB;YAeV,mBAAmB;YAyDnB,YAAY;YAUZ,mBAAmB;IAiBjC,OAAO,CAAC,sBAAsB;IAK9B,OAAO,CAAC,wBAAwB;IAuBhC,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,cAAc;CAUvB"}
1
+ {"version":3,"file":"clip-provider.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/clip-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AA+I/D,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,YAAY,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,WAAW,EAAE,kBAAkB,CAAC;IAChC,mBAAmB;IACnB,GAAG,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACpF;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;CAClC;AAED,qBAAa,YAAY;IAwBX,OAAO,CAAC,QAAQ,CAAC,IAAI;IAvBjC,OAAO,CAAC,OAAO,CAAyC;IACxD,OAAO,CAAC,MAAM,CAAwC;IACtD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,eAAe,CAAS;IAIhC,OAAO,CAAC,gBAAgB,CAAK;IAE7B,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAS;IACzC,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAU;IACzC,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAU;IAI9C,OAAO,CAAC,MAAM,CAAC,0BAA0B,CAAK;gBAEjB,IAAI,EAAE,mBAAmB;IAEtD,mFAAmF;IAC7E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B,wCAAwC;IAClC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAUjC,WAAW,IAAI,OAAO;YAQR,UAAU;IAuExB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,iBAAiB;IA6BzB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,cAAc;IAuBtB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,sBAAsB;IAyB9B,OAAO,CAAC,wBAAwB;IAWhC,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,gBAAgB;YAeV,mBAAmB;YA0EnB,YAAY;YAUZ,mBAAmB;IAiBjC,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,wBAAwB;IAuBhC,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,cAAc;CAUvB"}