@btraut/browser-bridge 0.13.0 → 0.13.2

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/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@ The format is based on "Keep a Changelog", and this project adheres to Semantic
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.13.2] - 2026-02-18
10
+
11
+ ### Changed
12
+
13
+ - Maintenance patch release for `0.13.2`.
14
+
15
+ ### Fixed
16
+
17
+ - MCP adapter readiness tests now model POST/GET health probing correctly, preventing false failures when daemon auto-start is enabled.
18
+
19
+ ## [0.13.1] - 2026-02-18
20
+
21
+ ### Fixed
22
+
23
+ - Extension packaging now resolves `@btraut/browser-bridge-shared/dist/*` imports from workspace source during zip builds, so release packaging works from clean CI checkouts without prebuilt shared artifacts.
24
+ - Core startup now handles shared-port collisions more clearly (POST/GET health compatibility probe + actionable occupied-port fallback), drive tab messaging retries transient post-navigation channel-closure races more aggressively, `drive.navigate` avoids false timeouts when URL commit succeeds without a DOM event, and diagnostics now surfaces inspect capability + shared-core metadata mismatch checks in one pass.
25
+
9
26
  ## [0.13.0] - 2026-02-18
10
27
 
11
28
  ### Changed
package/dist/api.js CHANGED
@@ -41,6 +41,7 @@ var import_express2 = __toESM(require("express"));
41
41
 
42
42
  // packages/shared/src/core-readiness.ts
43
43
  var import_promises = require("node:timers/promises");
44
+ var import_node_net = require("node:net");
44
45
 
45
46
  // packages/shared/src/logging.ts
46
47
  var import_node_fs2 = require("node:fs");
@@ -637,6 +638,27 @@ var DEFAULT_HEALTH_RETRY_MS = 250;
637
638
  var DEFAULT_HEALTH_ATTEMPTS = 20;
638
639
  var DEFAULT_HEALTH_TIMEOUT_MS = 2e3;
639
640
  var DEFAULT_HEALTH_BUDGET_MS = 15e3;
641
+ var DEFAULT_PORT_REACHABILITY_TIMEOUT_MS = 300;
642
+ var isPortReachableDefault = async (runtime) => {
643
+ return await new Promise((resolve3) => {
644
+ const socket = new import_node_net.Socket();
645
+ let settled = false;
646
+ const finish = (reachable) => {
647
+ if (settled) {
648
+ return;
649
+ }
650
+ settled = true;
651
+ socket.removeAllListeners();
652
+ socket.destroy();
653
+ resolve3(reachable);
654
+ };
655
+ socket.setTimeout(DEFAULT_PORT_REACHABILITY_TIMEOUT_MS);
656
+ socket.once("connect", () => finish(true));
657
+ socket.once("timeout", () => finish(false));
658
+ socket.once("error", () => finish(false));
659
+ socket.connect(runtime.port, runtime.host);
660
+ });
661
+ };
640
662
  var resolveTimeoutMs = (timeoutMs) => {
641
663
  const candidate = timeoutMs ?? (process.env.BROWSER_BRIDGE_CORE_TIMEOUT_MS ? Number.parseInt(process.env.BROWSER_BRIDGE_CORE_TIMEOUT_MS, 10) : process.env.BROWSER_VISION_CORE_TIMEOUT_MS ? Number.parseInt(process.env.BROWSER_VISION_CORE_TIMEOUT_MS, 10) : void 0);
642
664
  if (candidate === void 0 || candidate === null) {
@@ -702,57 +724,71 @@ var createCoreReadinessController = (options = {}) => {
702
724
  baseUrl = `http://${runtime.host}:${runtime.port}`;
703
725
  };
704
726
  const checkHealth = async () => {
705
- try {
706
- const controller = new AbortController();
707
- const timeout = setTimeout(() => controller.abort(), healthTimeoutMs);
727
+ for (const method of ["POST", "GET"]) {
708
728
  try {
709
- let response;
729
+ const controller = new AbortController();
730
+ const timeout = setTimeout(() => controller.abort(), healthTimeoutMs);
710
731
  try {
711
- response = await fetchImpl(`${baseUrl}/health`, {
712
- method: "POST",
713
- signal: controller.signal
714
- });
715
- } catch (error) {
716
- if (controller.signal.aborted || error instanceof Error && error.name === "AbortError") {
717
- logger.warn(`${logPrefix}.health.timeout`, {
732
+ let response;
733
+ try {
734
+ response = await fetchImpl(`${baseUrl}/health`, {
735
+ method,
736
+ signal: controller.signal
737
+ });
738
+ } catch (error) {
739
+ if (controller.signal.aborted || error instanceof Error && error.name === "AbortError") {
740
+ logger.warn(`${logPrefix}.health.timeout`, {
741
+ base_url: baseUrl,
742
+ method,
743
+ timeout_ms: healthTimeoutMs
744
+ });
745
+ continue;
746
+ }
747
+ logger.warn(`${logPrefix}.health.fetch_failed`, {
718
748
  base_url: baseUrl,
719
- timeout_ms: healthTimeoutMs
749
+ method,
750
+ error
720
751
  });
721
- return false;
752
+ throw error;
753
+ }
754
+ if (!response.ok) {
755
+ logger.warn(`${logPrefix}.health.non_ok`, {
756
+ base_url: baseUrl,
757
+ method,
758
+ status: response.status
759
+ });
760
+ continue;
761
+ }
762
+ const data = await response.json().catch(() => null);
763
+ const ok = Boolean(data?.ok);
764
+ if (ok) {
765
+ if (method === "GET") {
766
+ logger.info(`${logPrefix}.health.compat_probe`, {
767
+ base_url: baseUrl,
768
+ method
769
+ });
770
+ }
771
+ return true;
722
772
  }
723
- logger.warn(`${logPrefix}.health.fetch_failed`, {
724
- base_url: baseUrl,
725
- error
726
- });
727
- throw error;
728
- }
729
- if (!response.ok) {
730
- logger.warn(`${logPrefix}.health.non_ok`, {
731
- base_url: baseUrl,
732
- status: response.status
733
- });
734
- return false;
735
- }
736
- const data = await response.json().catch(() => null);
737
- const ok = Boolean(data?.ok);
738
- if (!ok) {
739
773
  logger.warn(`${logPrefix}.health.not_ready`, {
740
- base_url: baseUrl
774
+ base_url: baseUrl,
775
+ method
741
776
  });
777
+ } finally {
778
+ clearTimeout(timeout);
742
779
  }
743
- return ok;
744
- } finally {
745
- clearTimeout(timeout);
780
+ } catch (error) {
781
+ logger.warn(`${logPrefix}.health.error`, {
782
+ base_url: baseUrl,
783
+ method,
784
+ error
785
+ });
746
786
  }
747
- } catch (error) {
748
- logger.warn(`${logPrefix}.health.error`, {
749
- base_url: baseUrl,
750
- error
751
- });
752
- return false;
753
787
  }
788
+ return false;
754
789
  };
755
790
  const ensureCoreRunning = async () => {
791
+ const portReachabilityCheck = options.portReachabilityCheck ?? isPortReachableDefault;
756
792
  refreshRuntime();
757
793
  if (await checkHealth()) {
758
794
  logger.debug(`${logPrefix}.ensure_ready.already_running`, {
@@ -793,6 +829,21 @@ var createCoreReadinessController = (options = {}) => {
793
829
  health_budget_ms: healthBudgetMs,
794
830
  health_timeout_ms: healthTimeoutMs
795
831
  });
832
+ let portOccupied = false;
833
+ try {
834
+ portOccupied = await portReachabilityCheck(runtime);
835
+ } catch (error) {
836
+ logger.warn(`${logPrefix}.ensure_ready.port_probe_failed`, {
837
+ host: runtime.host,
838
+ port: runtime.port,
839
+ error
840
+ });
841
+ }
842
+ if (portOccupied) {
843
+ throw new Error(
844
+ `Core daemon failed to start on ${runtime.host}:${runtime.port}. A process is already listening on this port but did not pass Browser Bridge health checks. Retry with --no-daemon to reuse it, or enable isolated mode (BROWSER_BRIDGE_ISOLATED_MODE=1) for per-worktree ports.`
845
+ );
846
+ }
796
847
  throw new Error(
797
848
  `Core daemon failed to start on ${runtime.host}:${runtime.port}.`
798
849
  );
@@ -4703,6 +4754,13 @@ var endpointLabel = (endpoint) => {
4703
4754
  }
4704
4755
  return "unknown";
4705
4756
  };
4757
+ var readCapability = (capabilities, name) => {
4758
+ if (!capabilities) {
4759
+ return void 0;
4760
+ }
4761
+ const candidate = capabilities[name];
4762
+ return typeof candidate === "boolean" ? candidate : void 0;
4763
+ };
4706
4764
  var hasEndpoint = (endpoint) => Boolean(
4707
4765
  endpoint && typeof endpoint.host === "string" && endpoint.host.length > 0 && typeof endpoint.port === "number" && Number.isFinite(endpoint.port)
4708
4766
  );
@@ -4781,6 +4839,23 @@ var buildDiagnosticReport = (sessionId, context = {}) => {
4781
4839
  }
4782
4840
  });
4783
4841
  }
4842
+ if (callerEndpoint?.metadataPath && coreEndpoint?.metadataPath) {
4843
+ const matches = callerEndpoint.metadataPath === coreEndpoint.metadataPath;
4844
+ checks.push({
4845
+ name: "runtime.caller.metadata_path_match",
4846
+ ok: matches,
4847
+ message: matches ? "Caller metadata path matches the active core runtime metadata path." : "Caller metadata path differs from core runtime metadata path (shared core across worktrees).",
4848
+ details: {
4849
+ caller_metadata_path: callerEndpoint.metadataPath,
4850
+ core_metadata_path: coreEndpoint.metadataPath
4851
+ }
4852
+ });
4853
+ if (!matches) {
4854
+ warnings.push(
4855
+ "CLI is talking to a core process started from a different worktree metadata path. If daemon auto-start fails, retry with --no-daemon or enable isolated mode."
4856
+ );
4857
+ }
4858
+ }
4784
4859
  if (extensionConnected && hasEndpoint(coreEndpoint) && hasEndpoint(extensionEndpoint)) {
4785
4860
  const matches = coreEndpoint.host === extensionEndpoint.host && coreEndpoint.port === extensionEndpoint.port;
4786
4861
  checks.push({
@@ -4798,6 +4873,49 @@ var buildDiagnosticReport = (sessionId, context = {}) => {
4798
4873
  }
4799
4874
  });
4800
4875
  }
4876
+ if (extensionConnected) {
4877
+ const capabilityNegotiated = context.runtime?.extension?.capabilityNegotiated ?? false;
4878
+ const capabilities = context.runtime?.extension?.capabilities;
4879
+ const driveNavigateCapability = readCapability(
4880
+ capabilities,
4881
+ "drive.navigate"
4882
+ );
4883
+ const inspectAttachCapability = readCapability(
4884
+ capabilities,
4885
+ "debugger.attach"
4886
+ );
4887
+ const inspectCommandCapability = readCapability(
4888
+ capabilities,
4889
+ "debugger.command"
4890
+ );
4891
+ checks.push({
4892
+ name: "runtime.extension.capability_negotiated",
4893
+ ok: capabilityNegotiated,
4894
+ message: capabilityNegotiated ? "Extension capability negotiation completed." : "Extension capability negotiation is incomplete; action availability may be stale."
4895
+ });
4896
+ checks.push({
4897
+ name: "drive.capability",
4898
+ ok: driveNavigateCapability !== false,
4899
+ message: driveNavigateCapability === false ? "Drive actions are disabled by extension capability negotiation." : "Drive actions are available.",
4900
+ details: driveNavigateCapability === void 0 ? { capability: "drive.navigate", state: "unknown" } : { capability: "drive.navigate", enabled: driveNavigateCapability }
4901
+ });
4902
+ const inspectEnabled = inspectAttachCapability === true && inspectCommandCapability === true;
4903
+ checks.push({
4904
+ name: "inspect.capability",
4905
+ ok: capabilityNegotiated && inspectEnabled,
4906
+ message: capabilityNegotiated && inspectEnabled ? "Inspect debugger capability is enabled." : capabilityNegotiated ? "Inspect debugger capability is disabled in extension options." : "Inspect capability is unknown until extension capability negotiation completes.",
4907
+ details: {
4908
+ required_capabilities: ["debugger.attach", "debugger.command"],
4909
+ debugger_attach: inspectAttachCapability,
4910
+ debugger_command: inspectCommandCapability
4911
+ }
4912
+ });
4913
+ if (capabilityNegotiated && !inspectEnabled) {
4914
+ warnings.push(
4915
+ "Inspect commands require debugger capability. Enable debugger-based inspect in extension options to use inspect.* routes."
4916
+ );
4917
+ }
4918
+ }
4801
4919
  const callerVersion = context.runtime?.caller?.process?.version;
4802
4920
  const extensionVersion = extensionConnected ? context.runtime?.extension?.version : void 0;
4803
4921
  if (callerVersion && extensionVersion) {