@browserbridge/bbx 1.7.7 → 1.8.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/PRIVACY.md +111 -0
- package/README.md +13 -5
- package/RESPONSIBLE_USE.md +16 -0
- package/SECURITY.md +52 -0
- package/package.json +12 -6
- package/packages/agent-client/src/cli-args.js +24 -0
- package/packages/agent-client/src/cli.js +46 -52
- package/packages/agent-client/src/client.js +140 -5
- package/packages/agent-client/src/command-registry.js +56 -53
- package/packages/agent-client/src/content-extract.js +198 -0
- package/packages/agent-client/src/remotes.js +1 -1
- package/packages/agent-client/src/runtime.js +768 -35
- package/packages/agent-client/src/types.ts +131 -6
- package/packages/mcp-server/src/guidance.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +41 -5
- package/packages/mcp-server/src/handlers-dom.js +8 -2
- package/packages/mcp-server/src/handlers-navigation.js +2 -1
- package/packages/mcp-server/src/handlers-page.js +41 -4
- package/packages/mcp-server/src/handlers-utils.js +67 -7
- package/packages/mcp-server/src/handlers.js +4 -4
- package/packages/mcp-server/src/server.js +102 -6
- package/packages/native-host/src/daemon.js +158 -6
- package/packages/native-host/src/native-host.js +3 -1
- package/packages/protocol/src/defaults.js +3 -0
- package/packages/protocol/src/errors.js +54 -1
- package/packages/protocol/src/protocol.js +297 -1
- package/packages/protocol/src/registry.js +66 -35
- package/packages/protocol/src/summary.js +87 -5
- package/packages/protocol/src/types.ts +234 -0
- package/skills/browser-bridge/SKILL.md +34 -13
- package/skills/browser-bridge/references/interaction.md +55 -11
- package/skills/browser-bridge/references/patch-workflow.md +2 -0
- package/skills/browser-bridge/references/protocol.md +157 -82
- package/skills/browser-bridge/references/token-efficiency.md +19 -13
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
getBridgeDir,
|
|
10
10
|
getDaemonLogPath,
|
|
11
11
|
getDaemonPidPath,
|
|
12
|
+
getBridgeTransport,
|
|
12
13
|
getManifestInstallDir,
|
|
13
14
|
getSocketPath,
|
|
15
|
+
readProxyConfig,
|
|
14
16
|
SUPPORTED_BROWSERS,
|
|
15
17
|
} from '../../native-host/src/config.js';
|
|
16
18
|
import {
|
|
@@ -20,26 +22,70 @@ import {
|
|
|
20
22
|
import { resolveDefaultExtensionId } from '../../native-host/src/install-manifest.js';
|
|
21
23
|
import {
|
|
22
24
|
BridgeError,
|
|
25
|
+
BRIDGE_METHOD_REGISTRY,
|
|
23
26
|
CLIENT_REQUEST_TIMEOUT_MARGIN_MS,
|
|
24
27
|
DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
|
|
25
28
|
getBridgeOperationTimeoutMs,
|
|
29
|
+
getProtocolVersion,
|
|
26
30
|
MAX_CLIENT_REQUEST_TIMEOUT_MS,
|
|
27
31
|
} from '../../protocol/src/index.js';
|
|
28
32
|
import { methodNeedsTab } from './cli-helpers.js';
|
|
29
33
|
import { BridgeClient } from './client.js';
|
|
34
|
+
import { readRemoteConfig } from './remotes.js';
|
|
35
|
+
import { collectSetupStatus } from './setup-status.js';
|
|
30
36
|
|
|
31
37
|
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
32
38
|
/** @typedef {import('./types.js').BridgeMeta} BridgeMeta */
|
|
33
39
|
/** @typedef {import('./types.js').BridgeRequestSource} BridgeRequestSource */
|
|
34
40
|
/** @typedef {import('./types.js').BridgeResponse} BridgeResponse */
|
|
41
|
+
/** @typedef {import('./types.js').BridgeClientOptions} BridgeClientOptions */
|
|
35
42
|
/** @typedef {import('../../native-host/src/config.js').SupportedBrowser} SupportedBrowser */
|
|
36
43
|
/** @typedef {import('./types.js').BrowserManifestStatus} BrowserManifestStatus */
|
|
37
44
|
/** @typedef {import('./types.js').NativeHostManifestIssue} NativeHostManifestIssue */
|
|
38
45
|
/** @typedef {import('./types.js').DoctorReport} DoctorReport */
|
|
39
46
|
/** @typedef {import('./types.js').DoctorReportOptions} DoctorReportOptions */
|
|
47
|
+
/** @typedef {import('./types.js').DoctorRecentEvent} DoctorRecentEvent */
|
|
40
48
|
|
|
41
49
|
const CHROMIUM_SANDBOXED_MANIFEST_RE =
|
|
42
50
|
/(?:^|[/\\])(?:snap[/\\]chromium|\.var[/\\]app[/\\]org\.chromium\.Chromium)[/\\]/;
|
|
51
|
+
const DOCTOR_LOG_LIMIT = 12;
|
|
52
|
+
const DOCTOR_RECENT_EVENT_LIMIT = 10;
|
|
53
|
+
const DOCTOR_SETUP_ENTRY_LIMIT = 100;
|
|
54
|
+
const MAX_DOCTOR_COUNT = 1_000_000_000;
|
|
55
|
+
const SAFE_VERSION_RE = /^\d+\.\d+(?:\.\d+)?(?:[-+][A-Za-z0-9.-]+)?$/u;
|
|
56
|
+
const CHROME_EXTENSION_ID_RE = /^[a-p]{32}$/u;
|
|
57
|
+
const SAFE_DEBUGGER_STATES = new Set(['idle', 'active', 'conflict', 'detached']);
|
|
58
|
+
const SAFE_ROUTE_REASONS = new Set([
|
|
59
|
+
'enabled',
|
|
60
|
+
'access_disabled',
|
|
61
|
+
'enabled_window_missing',
|
|
62
|
+
'no_routable_active_tab',
|
|
63
|
+
'reconnected',
|
|
64
|
+
'restricted_page',
|
|
65
|
+
]);
|
|
66
|
+
const SAFE_CAPTURE_STATES = new Set([
|
|
67
|
+
'idle',
|
|
68
|
+
'stopped',
|
|
69
|
+
'armed',
|
|
70
|
+
'active',
|
|
71
|
+
'capturing',
|
|
72
|
+
'stop_failed',
|
|
73
|
+
'unavailable',
|
|
74
|
+
]);
|
|
75
|
+
const BRIDGE_METHOD_SET = new Set(Object.keys(BRIDGE_METHOD_REGISTRY));
|
|
76
|
+
const DOCTOR_DIAGNOSTIC_METHODS = new Set([
|
|
77
|
+
'health.ping',
|
|
78
|
+
'setup.get_status',
|
|
79
|
+
'log.tail',
|
|
80
|
+
'daemon.metrics',
|
|
81
|
+
]);
|
|
82
|
+
const SAFE_DEBUGGER_REASONS = new Set([
|
|
83
|
+
'debugger_conflict',
|
|
84
|
+
'debugger_detached',
|
|
85
|
+
'debugger_replaced',
|
|
86
|
+
'debugger_canceled',
|
|
87
|
+
'target_closed',
|
|
88
|
+
]);
|
|
43
89
|
|
|
44
90
|
/**
|
|
45
91
|
* @param {BridgeClient} client
|
|
@@ -135,10 +181,11 @@ function withRequestMeta(source, tokenBudget) {
|
|
|
135
181
|
/**
|
|
136
182
|
* @template T
|
|
137
183
|
* @param {(client: BridgeClient) => Promise<T>} callback
|
|
184
|
+
* @param {BridgeClientOptions} [options]
|
|
138
185
|
* @returns {Promise<T>}
|
|
139
186
|
*/
|
|
140
|
-
export async function withBridgeClient(callback) {
|
|
141
|
-
const client = new BridgeClient();
|
|
187
|
+
export async function withBridgeClient(callback, options) {
|
|
188
|
+
const client = new BridgeClient(options);
|
|
142
189
|
await ensureClientConnected(client);
|
|
143
190
|
try {
|
|
144
191
|
return await callback(client);
|
|
@@ -188,6 +235,44 @@ export async function checkBrowserManifests() {
|
|
|
188
235
|
);
|
|
189
236
|
}
|
|
190
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Read the allowlisted extension IDs from every installed browser manifest.
|
|
240
|
+
* Invalid manifests are already reported by the native-host health check and
|
|
241
|
+
* do not prevent the remaining browsers from contributing identity data.
|
|
242
|
+
*
|
|
243
|
+
* @param {BrowserManifestStatus[]} browserManifests
|
|
244
|
+
* @param {typeof fs.promises.readFile} [readFile]
|
|
245
|
+
* @returns {Promise<string[]>}
|
|
246
|
+
*/
|
|
247
|
+
export async function readInstalledExtensionIds(
|
|
248
|
+
browserManifests,
|
|
249
|
+
readFile = fs.promises.readFile.bind(fs.promises)
|
|
250
|
+
) {
|
|
251
|
+
/** @type {Set<string>} */
|
|
252
|
+
const extensionIds = new Set();
|
|
253
|
+
for (const entry of browserManifests.slice(0, DOCTOR_SETUP_ENTRY_LIMIT)) {
|
|
254
|
+
if (!entry.installed || extensionIds.size >= DOCTOR_SETUP_ENTRY_LIMIT) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const parsed = JSON.parse(await readFile(entry.manifestPath, 'utf8'));
|
|
259
|
+
const origins = Array.isArray(parsed?.allowed_origins) ? parsed.allowed_origins : [];
|
|
260
|
+
for (const origin of origins.slice(0, DOCTOR_SETUP_ENTRY_LIMIT)) {
|
|
261
|
+
const match =
|
|
262
|
+
typeof origin === 'string'
|
|
263
|
+
? /^chrome-extension:\/\/([a-p]{32})\/(?:\*)?$/u.exec(origin)
|
|
264
|
+
: null;
|
|
265
|
+
if (match?.[1]) {
|
|
266
|
+
extensionIds.add(match[1]);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
// The native-host manifest health collector reports malformed files.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return [...extensionIds];
|
|
274
|
+
}
|
|
275
|
+
|
|
191
276
|
/**
|
|
192
277
|
* @param {string} line
|
|
193
278
|
* @param {number} startIndex
|
|
@@ -377,6 +462,381 @@ export async function checkUnwritableBridgePaths() {
|
|
|
377
462
|
return unwritable;
|
|
378
463
|
}
|
|
379
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Doctor must never restart a daemon while it is inspecting version drift.
|
|
467
|
+
*
|
|
468
|
+
* @template T
|
|
469
|
+
* @param {(client: BridgeClient) => Promise<T>} callback
|
|
470
|
+
* @returns {Promise<T>}
|
|
471
|
+
*/
|
|
472
|
+
async function withDoctorBridgeClient(callback) {
|
|
473
|
+
const client = new BridgeClient({ restartDaemonOnVersionMismatch: false });
|
|
474
|
+
try {
|
|
475
|
+
await ensureClientConnected(client);
|
|
476
|
+
return await callback(client);
|
|
477
|
+
} finally {
|
|
478
|
+
await client.close().catch(() => {});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* @param {unknown} value
|
|
484
|
+
* @returns {Record<string, unknown> | null}
|
|
485
|
+
*/
|
|
486
|
+
function asRecord(value) {
|
|
487
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
488
|
+
? /** @type {Record<string, unknown>} */ (value)
|
|
489
|
+
: null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* @param {unknown} value
|
|
494
|
+
* @returns {number}
|
|
495
|
+
*/
|
|
496
|
+
function boundedCount(value) {
|
|
497
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
498
|
+
? Math.min(MAX_DOCTOR_COUNT, Math.trunc(value))
|
|
499
|
+
: 0;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* @param {unknown} value
|
|
504
|
+
* @returns {string | null}
|
|
505
|
+
*/
|
|
506
|
+
function safeVersion(value) {
|
|
507
|
+
return typeof value === 'string' && value.length <= 32 && SAFE_VERSION_RE.test(value)
|
|
508
|
+
? value
|
|
509
|
+
: null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* @param {unknown} value
|
|
514
|
+
* @returns {string[]}
|
|
515
|
+
*/
|
|
516
|
+
function safeVersions(value) {
|
|
517
|
+
if (!Array.isArray(value)) {
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
/** @type {Set<string>} */
|
|
521
|
+
const versions = new Set();
|
|
522
|
+
for (const candidate of value) {
|
|
523
|
+
const version = safeVersion(candidate);
|
|
524
|
+
if (version) {
|
|
525
|
+
versions.add(version);
|
|
526
|
+
}
|
|
527
|
+
if (versions.size === 10) {
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return [...versions];
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* @param {string} left
|
|
536
|
+
* @param {string} right
|
|
537
|
+
* @returns {number}
|
|
538
|
+
*/
|
|
539
|
+
function compareVersions(left, right) {
|
|
540
|
+
const leftParts = left.split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
541
|
+
const rightParts = right.split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
542
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
543
|
+
for (let index = 0; index < length; index += 1) {
|
|
544
|
+
const difference = (leftParts[index] || 0) - (rightParts[index] || 0);
|
|
545
|
+
if (difference !== 0) {
|
|
546
|
+
return difference > 0 ? 1 : -1;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Convert untrusted daemon log entries into a fixed allowlisted shape. Raw
|
|
554
|
+
* messages are inspected only to derive known cause categories and are never
|
|
555
|
+
* returned.
|
|
556
|
+
*
|
|
557
|
+
* @param {unknown} value
|
|
558
|
+
* @returns {DoctorRecentEvent['cause'] | undefined}
|
|
559
|
+
*/
|
|
560
|
+
function classifyRecentCause(value) {
|
|
561
|
+
const entry = asRecord(value);
|
|
562
|
+
if (!entry) {
|
|
563
|
+
return undefined;
|
|
564
|
+
}
|
|
565
|
+
const error = asRecord(entry.error);
|
|
566
|
+
const candidates = [
|
|
567
|
+
entry.cause,
|
|
568
|
+
entry.reason,
|
|
569
|
+
entry.errorCode,
|
|
570
|
+
entry.code,
|
|
571
|
+
entry.message,
|
|
572
|
+
entry.summary,
|
|
573
|
+
error?.code,
|
|
574
|
+
error?.message,
|
|
575
|
+
]
|
|
576
|
+
.filter((candidate) => typeof candidate === 'string')
|
|
577
|
+
.map((candidate) => candidate.slice(0, 200))
|
|
578
|
+
.join(' ')
|
|
579
|
+
.slice(0, 1000);
|
|
580
|
+
if (/another debugger|debugger.{0,40}(?:already attached|conflict|in use)/iu.test(candidates)) {
|
|
581
|
+
return 'debugger_conflict';
|
|
582
|
+
}
|
|
583
|
+
if (/debugger.{0,40}detach|detach.{0,40}debugger/iu.test(candidates)) {
|
|
584
|
+
return 'debugger_detached';
|
|
585
|
+
}
|
|
586
|
+
if (/DIALOG_ACTION_CONFLICT/iu.test(candidates)) {
|
|
587
|
+
return 'dialog_conflict';
|
|
588
|
+
}
|
|
589
|
+
if (/EXTENSION_DISCONNECTED|extension.{0,40}disconnect/iu.test(candidates)) {
|
|
590
|
+
return 'extension_disconnected';
|
|
591
|
+
}
|
|
592
|
+
if (/TAB_MISMATCH|wrong.{0,20}window|enabled window.{0,30}mismatch/iu.test(candidates)) {
|
|
593
|
+
return 'wrong_window';
|
|
594
|
+
}
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* @param {unknown} value
|
|
600
|
+
* @returns {DoctorRecentEvent[]}
|
|
601
|
+
*/
|
|
602
|
+
function summarizeRecentEvents(value) {
|
|
603
|
+
const result = asRecord(value);
|
|
604
|
+
const entries = Array.isArray(result?.entries)
|
|
605
|
+
? result.entries
|
|
606
|
+
.slice(-DOCTOR_LOG_LIMIT)
|
|
607
|
+
.filter((candidate) => {
|
|
608
|
+
const entry = asRecord(candidate);
|
|
609
|
+
return !DOCTOR_DIAGNOSTIC_METHODS.has(
|
|
610
|
+
typeof entry?.method === 'string' ? entry.method : ''
|
|
611
|
+
);
|
|
612
|
+
})
|
|
613
|
+
.slice(-DOCTOR_RECENT_EVENT_LIMIT)
|
|
614
|
+
: [];
|
|
615
|
+
/** @type {DoctorRecentEvent[]} */
|
|
616
|
+
const events = [];
|
|
617
|
+
for (const candidate of entries) {
|
|
618
|
+
const entry = asRecord(candidate);
|
|
619
|
+
if (!entry) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
/** @type {DoctorRecentEvent} */
|
|
623
|
+
const event = {};
|
|
624
|
+
if (typeof entry.at === 'string' && /^\d{4}-\d{2}-\d{2}T/u.test(entry.at)) {
|
|
625
|
+
const timestamp = Date.parse(entry.at);
|
|
626
|
+
if (Number.isFinite(timestamp)) {
|
|
627
|
+
event.at = new Date(timestamp).toISOString();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (typeof entry.method === 'string' && BRIDGE_METHOD_SET.has(entry.method)) {
|
|
631
|
+
event.method = /** @type {BridgeMethod} */ (entry.method);
|
|
632
|
+
}
|
|
633
|
+
if (typeof entry.ok === 'boolean') {
|
|
634
|
+
event.ok = entry.ok;
|
|
635
|
+
}
|
|
636
|
+
if (entry.source === 'cli' || entry.source === 'mcp') {
|
|
637
|
+
event.source = entry.source;
|
|
638
|
+
}
|
|
639
|
+
event.cause = classifyRecentCause(entry);
|
|
640
|
+
if (Object.values(event).some((field) => field !== undefined)) {
|
|
641
|
+
events.push(event);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return events;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* @param {unknown} value
|
|
649
|
+
* @returns {import('./types.js').DoctorDaemonMetrics | null}
|
|
650
|
+
*/
|
|
651
|
+
function summarizeMetrics(value) {
|
|
652
|
+
const metrics = asRecord(value);
|
|
653
|
+
if (!metrics) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
uptimeMs: boundedCount(metrics.uptimeMs),
|
|
658
|
+
activeAgents: boundedCount(metrics.activeAgents),
|
|
659
|
+
activeExtensions: boundedCount(metrics.activeExtensions),
|
|
660
|
+
pendingRequests: boundedCount(metrics.pendingRequests),
|
|
661
|
+
requestsProcessed: boundedCount(metrics.requestsProcessed),
|
|
662
|
+
requestsFailed: boundedCount(metrics.requestsFailed),
|
|
663
|
+
avgResponseTimeMs: boundedCount(metrics.avgResponseTimeMs),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* @param {unknown} value
|
|
669
|
+
* @param {'daemon' | 'direct'} source
|
|
670
|
+
* @returns {import('./types.js').DoctorSetupSummary | null}
|
|
671
|
+
*/
|
|
672
|
+
function summarizeSetupStatus(value, source) {
|
|
673
|
+
const status = asRecord(value);
|
|
674
|
+
if (
|
|
675
|
+
(status?.scope !== 'global' && status?.scope !== 'local') ||
|
|
676
|
+
!Array.isArray(status.mcpClients) ||
|
|
677
|
+
!Array.isArray(status.skillTargets)
|
|
678
|
+
) {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
const mcpClients = status.mcpClients.slice(0, DOCTOR_SETUP_ENTRY_LIMIT).map(asRecord);
|
|
682
|
+
const skillTargets = status.skillTargets.slice(0, DOCTOR_SETUP_ENTRY_LIMIT).map(asRecord);
|
|
683
|
+
return {
|
|
684
|
+
source,
|
|
685
|
+
scope: status.scope,
|
|
686
|
+
mcp: {
|
|
687
|
+
detected: boundedCount(mcpClients.filter((client) => client?.detected === true).length),
|
|
688
|
+
configured: boundedCount(mcpClients.filter((client) => client?.configured === true).length),
|
|
689
|
+
},
|
|
690
|
+
skills: {
|
|
691
|
+
detected: boundedCount(skillTargets.filter((target) => target?.detected === true).length),
|
|
692
|
+
installed: boundedCount(skillTargets.filter((target) => target?.installed === true).length),
|
|
693
|
+
managed: boundedCount(skillTargets.filter((target) => target?.managed === true).length),
|
|
694
|
+
updatesAvailable: boundedCount(
|
|
695
|
+
skillTargets.filter((target) => target?.updateAvailable === true).length
|
|
696
|
+
),
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* @param {Record<string, unknown> | null} health
|
|
703
|
+
* @returns {import('./types.js').DoctorDebuggerDiagnostics}
|
|
704
|
+
*/
|
|
705
|
+
function summarizeDebugger(health) {
|
|
706
|
+
const debuggerHealth = asRecord(health?.debugger);
|
|
707
|
+
const captureHealth = asRecord(health?.capture);
|
|
708
|
+
const rawState = debuggerHealth?.state ?? debuggerHealth?.status ?? health?.debuggerState;
|
|
709
|
+
/** @type {import('./types.js').DoctorDebuggerDiagnostics['state']} */
|
|
710
|
+
let state =
|
|
711
|
+
typeof rawState === 'string' && SAFE_DEBUGGER_STATES.has(rawState)
|
|
712
|
+
? /** @type {'idle' | 'active' | 'conflict' | 'detached'} */ (rawState)
|
|
713
|
+
: 'unknown';
|
|
714
|
+
if (debuggerHealth?.conflict === true) {
|
|
715
|
+
state = 'conflict';
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const attachedTabs = debuggerHealth?.attachedTabs;
|
|
719
|
+
const attachedTabCount = Array.isArray(attachedTabs)
|
|
720
|
+
? boundedCount(attachedTabs.length)
|
|
721
|
+
: typeof debuggerHealth?.attachedTabCount === 'number'
|
|
722
|
+
? boundedCount(debuggerHealth.attachedTabCount)
|
|
723
|
+
: null;
|
|
724
|
+
const heldTabCount =
|
|
725
|
+
typeof debuggerHealth?.heldTabCount === 'number'
|
|
726
|
+
? boundedCount(debuggerHealth.heldTabCount)
|
|
727
|
+
: null;
|
|
728
|
+
const pendingTabCount =
|
|
729
|
+
typeof debuggerHealth?.pendingTabCount === 'number'
|
|
730
|
+
? boundedCount(debuggerHealth.pendingTabCount)
|
|
731
|
+
: null;
|
|
732
|
+
const recentReason =
|
|
733
|
+
typeof debuggerHealth?.recentReason === 'string' &&
|
|
734
|
+
SAFE_DEBUGGER_REASONS.has(debuggerHealth.recentReason)
|
|
735
|
+
? /** @type {import('./types.js').DoctorDebuggerDiagnostics['recentReason']} */ (
|
|
736
|
+
debuggerHealth.recentReason
|
|
737
|
+
)
|
|
738
|
+
: null;
|
|
739
|
+
const rawCaptureState =
|
|
740
|
+
captureHealth?.state ?? debuggerHealth?.captureState ?? health?.captureState;
|
|
741
|
+
const captureState =
|
|
742
|
+
typeof rawCaptureState === 'string' && SAFE_CAPTURE_STATES.has(rawCaptureState)
|
|
743
|
+
? /** @type {import('./types.js').DoctorDebuggerDiagnostics['captureState']} */ (
|
|
744
|
+
rawCaptureState
|
|
745
|
+
)
|
|
746
|
+
: 'unknown';
|
|
747
|
+
const captureActiveTabCount =
|
|
748
|
+
typeof captureHealth?.activeTabCount === 'number'
|
|
749
|
+
? boundedCount(captureHealth.activeTabCount)
|
|
750
|
+
: null;
|
|
751
|
+
const captureOwnershipCount =
|
|
752
|
+
typeof captureHealth?.ownershipCount === 'number'
|
|
753
|
+
? boundedCount(captureHealth.ownershipCount)
|
|
754
|
+
: null;
|
|
755
|
+
const captureInflightCount =
|
|
756
|
+
typeof captureHealth?.inflightCount === 'number'
|
|
757
|
+
? boundedCount(captureHealth.inflightCount)
|
|
758
|
+
: null;
|
|
759
|
+
const interceptionActiveTabCount =
|
|
760
|
+
typeof captureHealth?.interceptionActiveTabCount === 'number'
|
|
761
|
+
? boundedCount(captureHealth.interceptionActiveTabCount)
|
|
762
|
+
: null;
|
|
763
|
+
const interceptionRuleCount =
|
|
764
|
+
typeof captureHealth?.interceptionRuleCount === 'number'
|
|
765
|
+
? boundedCount(captureHealth.interceptionRuleCount)
|
|
766
|
+
: null;
|
|
767
|
+
return {
|
|
768
|
+
state,
|
|
769
|
+
attachedTabCount,
|
|
770
|
+
heldTabCount,
|
|
771
|
+
pendingTabCount,
|
|
772
|
+
recentReason,
|
|
773
|
+
captureState,
|
|
774
|
+
captureActiveTabCount,
|
|
775
|
+
captureOwnershipCount,
|
|
776
|
+
captureInflightCount,
|
|
777
|
+
interceptionActiveTabCount,
|
|
778
|
+
interceptionRuleCount,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* @param {Record<string, unknown> | null} health
|
|
784
|
+
* @param {boolean} extensionConnected
|
|
785
|
+
* @returns {import('./types.js').DoctorProtocolDiagnostics}
|
|
786
|
+
*/
|
|
787
|
+
function summarizeProtocol(health, extensionConnected) {
|
|
788
|
+
const clientVersion = getProtocolVersion();
|
|
789
|
+
const advertisedDaemonVersions = safeVersions(health?.daemon_supported_versions);
|
|
790
|
+
const daemonSupportedVersions =
|
|
791
|
+
advertisedDaemonVersions.length > 0 || extensionConnected
|
|
792
|
+
? advertisedDaemonVersions
|
|
793
|
+
: safeVersions(health?.supported_versions);
|
|
794
|
+
const extensionSupportedVersions = extensionConnected
|
|
795
|
+
? Array.isArray(health?.extension_supported_versions)
|
|
796
|
+
? safeVersions(health.extension_supported_versions)
|
|
797
|
+
: safeVersions(health?.supported_versions)
|
|
798
|
+
: [];
|
|
799
|
+
const daemonCompatible = daemonSupportedVersions.length
|
|
800
|
+
? daemonSupportedVersions.includes(clientVersion)
|
|
801
|
+
: null;
|
|
802
|
+
const extensionCompatible = extensionConnected
|
|
803
|
+
? extensionSupportedVersions.length
|
|
804
|
+
? extensionSupportedVersions.includes(clientVersion)
|
|
805
|
+
: null
|
|
806
|
+
: null;
|
|
807
|
+
const knownCompatibility = [daemonCompatible, extensionCompatible].filter(
|
|
808
|
+
(value) => value !== null
|
|
809
|
+
);
|
|
810
|
+
const compatible = knownCompatibility.includes(false)
|
|
811
|
+
? false
|
|
812
|
+
: knownCompatibility.length
|
|
813
|
+
? true
|
|
814
|
+
: null;
|
|
815
|
+
|
|
816
|
+
/** @type {import('./types.js').DoctorProtocolDiagnostics['migration']} */
|
|
817
|
+
let migration = compatible === true ? 'none' : 'unknown';
|
|
818
|
+
const latestExtension = extensionSupportedVersions[0];
|
|
819
|
+
const latestDaemon = daemonSupportedVersions[0];
|
|
820
|
+
if (extensionCompatible === false && latestExtension) {
|
|
821
|
+
migration =
|
|
822
|
+
compareVersions(latestExtension, clientVersion) > 0 ? 'update_client' : 'update_extension';
|
|
823
|
+
} else if (daemonCompatible === false && latestDaemon) {
|
|
824
|
+
migration =
|
|
825
|
+
compareVersions(latestDaemon, clientVersion) > 0 ? 'update_client' : 'restart_daemon';
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return {
|
|
829
|
+
clientVersion,
|
|
830
|
+
daemonVersion: safeVersion(health?.daemonVersion),
|
|
831
|
+
daemonSupportedVersions,
|
|
832
|
+
extensionSupportedVersions,
|
|
833
|
+
daemonCompatible,
|
|
834
|
+
extensionCompatible,
|
|
835
|
+
compatible,
|
|
836
|
+
migration,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
380
840
|
/**
|
|
381
841
|
* @param {DoctorReportOptions} [options={}]
|
|
382
842
|
* @returns {Promise<DoctorReport>}
|
|
@@ -404,6 +864,53 @@ export async function getDoctorReport(options = {}) {
|
|
|
404
864
|
CHROMIUM_SANDBOXED_MANIFEST_RE.test(entry.manifestPath)
|
|
405
865
|
);
|
|
406
866
|
|
|
867
|
+
/** @type {'socket' | 'tcp' | 'unknown'} */
|
|
868
|
+
let transportKind = 'unknown';
|
|
869
|
+
let proxyConfigured = false;
|
|
870
|
+
/** @type {string[]} */
|
|
871
|
+
const diagnosticFailures = [];
|
|
872
|
+
/** @type {string[]} */
|
|
873
|
+
let installedExtensionIds = [];
|
|
874
|
+
try {
|
|
875
|
+
installedExtensionIds = await (options.readInstalledExtensionIds || readInstalledExtensionIds)(
|
|
876
|
+
browserManifests
|
|
877
|
+
);
|
|
878
|
+
installedExtensionIds = installedExtensionIds
|
|
879
|
+
.filter((extensionId) => CHROME_EXTENSION_ID_RE.test(extensionId))
|
|
880
|
+
.slice(0, DOCTOR_SETUP_ENTRY_LIMIT);
|
|
881
|
+
} catch {
|
|
882
|
+
diagnosticFailures.push('manifest_identity_unavailable');
|
|
883
|
+
}
|
|
884
|
+
try {
|
|
885
|
+
transportKind = (options.getLocalTransport || getBridgeTransport)().type;
|
|
886
|
+
} catch {
|
|
887
|
+
diagnosticFailures.push('transport_config_unavailable');
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
proxyConfigured = Boolean((options.readProxyConfig || readProxyConfig)());
|
|
891
|
+
} catch {
|
|
892
|
+
diagnosticFailures.push('proxy_config_unavailable');
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** @type {import('./types.js').DoctorRemoteDiagnostics} */
|
|
896
|
+
let remoteDestinations = {
|
|
897
|
+
configuredCount: 0,
|
|
898
|
+
status: 'not_configured',
|
|
899
|
+
credentials: 'not_configured',
|
|
900
|
+
};
|
|
901
|
+
try {
|
|
902
|
+
const remoteConfig = await (options.readRemoteConfig || readRemoteConfig)();
|
|
903
|
+
const configuredCount = remoteConfig.remotes.length;
|
|
904
|
+
remoteDestinations = {
|
|
905
|
+
configuredCount,
|
|
906
|
+
status: configuredCount > 0 ? 'not_probed_local_only' : 'not_configured',
|
|
907
|
+
credentials: configuredCount > 0 ? 'unverified' : 'not_configured',
|
|
908
|
+
};
|
|
909
|
+
} catch {
|
|
910
|
+
diagnosticFailures.push('remote_config_unavailable');
|
|
911
|
+
remoteDestinations.status = 'config_unavailable';
|
|
912
|
+
}
|
|
913
|
+
|
|
407
914
|
/** @type {DoctorReport} */
|
|
408
915
|
const report = {
|
|
409
916
|
manifestInstalled,
|
|
@@ -412,6 +919,7 @@ export async function getDoctorReport(options = {}) {
|
|
|
412
919
|
defaultExtensionId: defaultExtensionId.extensionId,
|
|
413
920
|
defaultExtensionIdSource: defaultExtensionId.source,
|
|
414
921
|
daemonReachable: false,
|
|
922
|
+
healthAvailable: false,
|
|
415
923
|
extensionConnected: false,
|
|
416
924
|
accessEnabled: false,
|
|
417
925
|
enabledWindowId: null,
|
|
@@ -425,35 +933,202 @@ export async function getDoctorReport(options = {}) {
|
|
|
425
933
|
issues: [],
|
|
426
934
|
nextSteps: [],
|
|
427
935
|
browserManifests,
|
|
936
|
+
transport: {
|
|
937
|
+
kind: transportKind,
|
|
938
|
+
local: true,
|
|
939
|
+
status: 'offline',
|
|
940
|
+
proxyConfigured,
|
|
941
|
+
proxyExposed: null,
|
|
942
|
+
credentials: transportKind === 'socket' ? 'not_required' : 'unknown',
|
|
943
|
+
},
|
|
944
|
+
connections: {
|
|
945
|
+
extensionCount: 0,
|
|
946
|
+
profileCount: 0,
|
|
947
|
+
},
|
|
948
|
+
protocol: summarizeProtocol(null, false),
|
|
949
|
+
debugger: summarizeDebugger(null),
|
|
950
|
+
metrics: null,
|
|
951
|
+
recentEvents: [],
|
|
952
|
+
recentCauses: [],
|
|
953
|
+
setup: null,
|
|
954
|
+
remoteDestinations,
|
|
955
|
+
diagnosticFailures,
|
|
428
956
|
};
|
|
429
957
|
|
|
958
|
+
/** @type {Record<string, unknown> | null} */
|
|
959
|
+
let healthResult = null;
|
|
960
|
+
/** @type {unknown} */
|
|
961
|
+
let setupStatus = null;
|
|
962
|
+
/** @type {Record<string, unknown> | null} */
|
|
963
|
+
let metricsResult = null;
|
|
964
|
+
/** @type {Record<string, unknown> | null} */
|
|
965
|
+
let logsResult = null;
|
|
966
|
+
/** @type {unknown} */
|
|
967
|
+
let connectionError = null;
|
|
968
|
+
|
|
430
969
|
try {
|
|
431
|
-
await (options.bridgeClientRunner ||
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
970
|
+
await (options.bridgeClientRunner || withDoctorBridgeClient)(async (client) => {
|
|
971
|
+
report.daemonReachable = true;
|
|
972
|
+
/**
|
|
973
|
+
* @param {BridgeMethod} method
|
|
974
|
+
* @param {Record<string, unknown>} [params]
|
|
975
|
+
* @returns {Promise<Record<string, unknown> | null>}
|
|
976
|
+
*/
|
|
977
|
+
async function requestDiagnostic(method, params = {}) {
|
|
978
|
+
try {
|
|
979
|
+
const response = await client.request({ method, params });
|
|
980
|
+
if (!response.ok) {
|
|
981
|
+
diagnosticFailures.push(`${method}_failed`);
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
report.daemonReachable = true;
|
|
985
|
+
return asRecord(response.result);
|
|
986
|
+
} catch {
|
|
987
|
+
diagnosticFailures.push(`${method}_failed`);
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
healthResult = await requestDiagnostic('health.ping');
|
|
993
|
+
if (options.includeSetupStatus !== false) {
|
|
994
|
+
const setupResult = await requestDiagnostic('setup.get_status');
|
|
995
|
+
if (setupResult) {
|
|
996
|
+
setupStatus = setupResult;
|
|
997
|
+
}
|
|
435
998
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
windowId?: number | null,
|
|
439
|
-
routeTabId?: number | null,
|
|
440
|
-
routeReady?: boolean,
|
|
441
|
-
reason?: string
|
|
442
|
-
} }} */ (response.result);
|
|
443
|
-
report.daemonReachable = result.daemon === 'ok';
|
|
444
|
-
report.extensionConnected = result.extensionConnected === true;
|
|
445
|
-
report.accessEnabled = result.access?.enabled === true;
|
|
446
|
-
report.enabledWindowId =
|
|
447
|
-
typeof result.access?.windowId === 'number' ? result.access.windowId : null;
|
|
448
|
-
report.routeTabId =
|
|
449
|
-
typeof result.access?.routeTabId === 'number' ? result.access.routeTabId : null;
|
|
450
|
-
report.routeReady = result.access?.routeReady === true;
|
|
451
|
-
report.routeReason =
|
|
452
|
-
typeof result.access?.reason === 'string' ? result.access.reason : 'access_disabled';
|
|
999
|
+
logsResult = await requestDiagnostic('log.tail', { limit: DOCTOR_LOG_LIMIT });
|
|
1000
|
+
metricsResult = await requestDiagnostic('daemon.metrics');
|
|
453
1001
|
});
|
|
454
|
-
} catch {
|
|
455
|
-
|
|
456
|
-
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
connectionError = error;
|
|
1004
|
+
diagnosticFailures.push('daemon_connection_failed');
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (!report.daemonReachable && options.includeSetupStatus !== false) {
|
|
1008
|
+
try {
|
|
1009
|
+
setupStatus = await (options.collectSetupStatus || collectSetupStatus)();
|
|
1010
|
+
const summary = summarizeSetupStatus(setupStatus, 'direct');
|
|
1011
|
+
if (summary) {
|
|
1012
|
+
report.setup = summary;
|
|
1013
|
+
} else {
|
|
1014
|
+
diagnosticFailures.push('setup_status_invalid');
|
|
1015
|
+
}
|
|
1016
|
+
} catch {
|
|
1017
|
+
diagnosticFailures.push('setup_direct_failed');
|
|
1018
|
+
}
|
|
1019
|
+
} else if (setupStatus) {
|
|
1020
|
+
const summary = summarizeSetupStatus(setupStatus, 'daemon');
|
|
1021
|
+
if (summary) {
|
|
1022
|
+
report.setup = summary;
|
|
1023
|
+
} else {
|
|
1024
|
+
diagnosticFailures.push('setup_status_invalid');
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const connectionMessage =
|
|
1029
|
+
connectionError instanceof Error ? connectionError.message.slice(0, 200) : '';
|
|
1030
|
+
const authenticationFailed = /authentication failed|access denied|invalid.{0,20}token/iu.test(
|
|
1031
|
+
connectionMessage
|
|
1032
|
+
);
|
|
1033
|
+
report.transport.status = authenticationFailed
|
|
1034
|
+
? 'authentication_failed'
|
|
1035
|
+
: report.daemonReachable
|
|
1036
|
+
? 'reachable'
|
|
1037
|
+
: 'offline';
|
|
1038
|
+
report.transport.credentials =
|
|
1039
|
+
transportKind === 'socket'
|
|
1040
|
+
? 'not_required'
|
|
1041
|
+
: authenticationFailed
|
|
1042
|
+
? 'rejected'
|
|
1043
|
+
: report.daemonReachable
|
|
1044
|
+
? 'accepted'
|
|
1045
|
+
: 'unknown';
|
|
1046
|
+
|
|
1047
|
+
const resolvedHealth = /** @type {Record<string, unknown> | null} */ (
|
|
1048
|
+
/** @type {unknown} */ (healthResult)
|
|
1049
|
+
);
|
|
1050
|
+
report.healthAvailable = resolvedHealth?.daemon === 'ok';
|
|
1051
|
+
if (
|
|
1052
|
+
report.daemonReachable &&
|
|
1053
|
+
!report.healthAvailable &&
|
|
1054
|
+
!diagnosticFailures.includes('health.ping_failed')
|
|
1055
|
+
) {
|
|
1056
|
+
diagnosticFailures.push('health.ping_invalid');
|
|
1057
|
+
}
|
|
1058
|
+
const healthAccess = report.healthAvailable ? asRecord(resolvedHealth?.access) : null;
|
|
1059
|
+
if (report.healthAvailable) {
|
|
1060
|
+
report.extensionConnected = resolvedHealth?.extensionConnected === true;
|
|
1061
|
+
report.accessEnabled = healthAccess?.enabled === true;
|
|
1062
|
+
report.enabledWindowId =
|
|
1063
|
+
typeof healthAccess?.windowId === 'number' ? boundedCount(healthAccess.windowId) : null;
|
|
1064
|
+
report.routeTabId =
|
|
1065
|
+
typeof healthAccess?.routeTabId === 'number' ? boundedCount(healthAccess.routeTabId) : null;
|
|
1066
|
+
report.routeReady = healthAccess?.routeReady === true;
|
|
1067
|
+
report.routeReason =
|
|
1068
|
+
typeof healthAccess?.reason === 'string' && SAFE_ROUTE_REASONS.has(healthAccess.reason)
|
|
1069
|
+
? healthAccess.reason
|
|
1070
|
+
: 'access_disabled';
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const connectedExtensions =
|
|
1074
|
+
report.healthAvailable && Array.isArray(resolvedHealth?.connectedExtensions)
|
|
1075
|
+
? resolvedHealth.connectedExtensions.slice(0, 100)
|
|
1076
|
+
: [];
|
|
1077
|
+
let profileCount = 0;
|
|
1078
|
+
for (const extension of connectedExtensions) {
|
|
1079
|
+
const extensionRecord = asRecord(extension);
|
|
1080
|
+
if (typeof extensionRecord?.profileLabel === 'string' && extensionRecord.profileLabel) {
|
|
1081
|
+
profileCount += 1;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
report.metrics = summarizeMetrics(metricsResult);
|
|
1085
|
+
report.connections.extensionCount = connectedExtensions.length
|
|
1086
|
+
? connectedExtensions.length
|
|
1087
|
+
: report.metrics?.activeExtensions || (report.extensionConnected ? 1 : 0);
|
|
1088
|
+
report.connections.profileCount = profileCount;
|
|
1089
|
+
if (report.healthAvailable && report.connections.extensionCount > 0) {
|
|
1090
|
+
report.extensionConnected = true;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const proxyHealth = report.healthAvailable ? asRecord(resolvedHealth?.proxy) : null;
|
|
1094
|
+
report.transport.proxyConfigured = proxyConfigured;
|
|
1095
|
+
report.transport.proxyExposed =
|
|
1096
|
+
typeof proxyHealth?.enabled === 'boolean' ? proxyHealth.enabled : null;
|
|
1097
|
+
report.protocol = summarizeProtocol(
|
|
1098
|
+
report.healthAvailable ? resolvedHealth : null,
|
|
1099
|
+
report.extensionConnected
|
|
1100
|
+
);
|
|
1101
|
+
report.recentEvents = summarizeRecentEvents(logsResult);
|
|
1102
|
+
report.recentCauses = [
|
|
1103
|
+
...new Set(
|
|
1104
|
+
report.recentEvents.map((event) => event.cause).filter((cause) => cause !== undefined)
|
|
1105
|
+
),
|
|
1106
|
+
];
|
|
1107
|
+
report.debugger = summarizeDebugger(report.healthAvailable ? resolvedHealth : null);
|
|
1108
|
+
if (report.debugger.recentReason && !report.recentCauses.includes(report.debugger.recentReason)) {
|
|
1109
|
+
report.recentCauses.push(report.debugger.recentReason);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Keep optional diagnostics deterministic even if several RPCs fail.
|
|
1113
|
+
report.diagnosticFailures = [...new Set(diagnosticFailures)].slice(0, 12);
|
|
1114
|
+
|
|
1115
|
+
const connectedBrowserExtensionIds = connectedExtensions
|
|
1116
|
+
.map((extension) => asRecord(extension)?.browserExtensionId)
|
|
1117
|
+
.filter(
|
|
1118
|
+
(extensionId) => typeof extensionId === 'string' && CHROME_EXTENSION_ID_RE.test(extensionId)
|
|
1119
|
+
);
|
|
1120
|
+
const extensionIdentityMismatch =
|
|
1121
|
+
connectedBrowserExtensionIds.length > 0 &&
|
|
1122
|
+
installedExtensionIds.length > 0 &&
|
|
1123
|
+
connectedBrowserExtensionIds.some(
|
|
1124
|
+
(extensionId) => !installedExtensionIds.includes(/** @type {string} */ (extensionId))
|
|
1125
|
+
);
|
|
1126
|
+
|
|
1127
|
+
if (authenticationFailed) {
|
|
1128
|
+
report.issues.push('proxy_credentials_stale');
|
|
1129
|
+
report.nextSteps.push(
|
|
1130
|
+
'Local TCP authentication failed. Run `bbx proxy enable --rotate-token`, restart the daemon, and update configured remote clients with `bbx remote add`.'
|
|
1131
|
+
);
|
|
457
1132
|
}
|
|
458
1133
|
|
|
459
1134
|
if (unwritableBridgePaths.length > 0) {
|
|
@@ -482,11 +1157,23 @@ export async function getDoctorReport(options = {}) {
|
|
|
482
1157
|
: 'Run `bbx install <extension-id>` (or `bbx install --all`) to install the native host manifest.'
|
|
483
1158
|
);
|
|
484
1159
|
}
|
|
485
|
-
if (!report.daemonReachable) {
|
|
1160
|
+
if (!report.daemonReachable && !authenticationFailed) {
|
|
486
1161
|
report.issues.push('daemon_offline');
|
|
487
1162
|
report.nextSteps.push('Run `bbx-daemon` and retry `bbx status` or `bbx doctor`.');
|
|
488
1163
|
}
|
|
489
|
-
if (report.daemonReachable && !report.
|
|
1164
|
+
if (report.daemonReachable && !report.healthAvailable) {
|
|
1165
|
+
report.issues.push('health_unavailable');
|
|
1166
|
+
report.nextSteps.push(
|
|
1167
|
+
'The daemon is reachable but `health.ping` did not return valid core health. Run `bbx restart`, then update the Browser Bridge CLI and extension if the health check still fails.'
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
if (report.healthAvailable && extensionIdentityMismatch) {
|
|
1171
|
+
report.issues.push('extension_id_mismatch');
|
|
1172
|
+
report.nextSteps.push(
|
|
1173
|
+
'A connected Chrome extension ID is not allowed by any installed native host manifest. Reinstall the matching browser manifest with `bbx install --browser <browser>` and reload that extension.'
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
if (report.healthAvailable && !report.extensionConnected) {
|
|
490
1177
|
report.issues.push('extension_disconnected');
|
|
491
1178
|
if (chromiumSandboxedManifestInstalled) {
|
|
492
1179
|
report.issues.push('chromium_sandboxed_native_host_limited');
|
|
@@ -495,20 +1182,66 @@ export async function getDoctorReport(options = {}) {
|
|
|
495
1182
|
);
|
|
496
1183
|
}
|
|
497
1184
|
report.nextSteps.push(
|
|
498
|
-
'Open Chrome and make sure the Browser Bridge extension is installed and active.'
|
|
1185
|
+
'Open Chrome and make sure the Browser Bridge extension is installed and active. If Chrome reports a missing native host, run `bbx install --all` and reload the extension.'
|
|
499
1186
|
);
|
|
500
1187
|
}
|
|
501
|
-
if (report.
|
|
502
|
-
report.
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1188
|
+
if (report.healthAvailable && report.extensionConnected && !report.accessEnabled) {
|
|
1189
|
+
if (report.routeReason === 'enabled_window_missing') {
|
|
1190
|
+
report.issues.push('enabled_window_missing');
|
|
1191
|
+
report.nextSteps.push(
|
|
1192
|
+
'The previously enabled window no longer exists. Focus the intended Chrome window and click Enable in the Browser Bridge popup or side panel.'
|
|
1193
|
+
);
|
|
1194
|
+
} else {
|
|
1195
|
+
report.issues.push('access_disabled');
|
|
1196
|
+
report.nextSteps.push(
|
|
1197
|
+
'If a Browser Bridge call returns ACCESS_DENIED, stop requesting access. Ask the user to focus the needed Chrome window and click Enable for the needed window, then tell you when that window is ready.'
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
} else if (report.healthAvailable && report.extensionConnected && !report.routeReady) {
|
|
507
1201
|
report.issues.push(report.routeReason || 'no_routable_active_tab');
|
|
508
1202
|
report.nextSteps.push(
|
|
509
1203
|
'Switch to a supported page in the enabled window, or use an explicit tabId override.'
|
|
510
1204
|
);
|
|
511
1205
|
}
|
|
512
1206
|
|
|
1207
|
+
if (report.protocol.compatible === false) {
|
|
1208
|
+
report.issues.push('protocol_mismatch');
|
|
1209
|
+
if (report.protocol.migration === 'update_extension') {
|
|
1210
|
+
report.nextSteps.push(
|
|
1211
|
+
'Update or reload the Browser Bridge extension so it supports the client protocol, then retry `bbx doctor`.'
|
|
1212
|
+
);
|
|
1213
|
+
} else if (report.protocol.migration === 'restart_daemon') {
|
|
1214
|
+
report.nextSteps.push(
|
|
1215
|
+
'Run `bbx restart` to replace the stale daemon with this installed CLI version, then retry `bbx doctor`.'
|
|
1216
|
+
);
|
|
1217
|
+
} else {
|
|
1218
|
+
report.nextSteps.push(
|
|
1219
|
+
'Update the Browser Bridge CLI/npm package, run `bbx restart`, and retry `bbx doctor`.'
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
if (report.debugger.state === 'conflict') {
|
|
1225
|
+
report.issues.push('debugger_conflict');
|
|
1226
|
+
}
|
|
1227
|
+
if (report.debugger.state === 'conflict' || report.recentCauses.includes('debugger_conflict')) {
|
|
1228
|
+
report.nextSteps.push(
|
|
1229
|
+
'Close DevTools or any other debugger attached to the target tab, then retry the debugger-backed Browser Bridge operation.'
|
|
1230
|
+
);
|
|
1231
|
+
} else if (
|
|
1232
|
+
report.debugger.state === 'detached' ||
|
|
1233
|
+
report.recentCauses.includes('debugger_detached')
|
|
1234
|
+
) {
|
|
1235
|
+
report.nextSteps.push(
|
|
1236
|
+
'A recent debugger session detached. Keep the target tab open and retry the debugger-backed operation; close competing debuggers if detaches continue.'
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (report.recentCauses.includes('wrong_window')) {
|
|
1241
|
+
report.nextSteps.push(
|
|
1242
|
+
'A recent request targeted the wrong window. Focus the intended Chrome window, click Enable there, and retry without reusing a tabId from another window.'
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
513
1246
|
return report;
|
|
514
1247
|
}
|