@reconcrap/boss-recommend-mcp 2.0.50 → 2.0.52

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.
@@ -1,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { execFile, spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -13,6 +13,7 @@ export const LID_CLOSED_SAFE_CHROME_ARGS = [
13
13
  "--disable-renderer-backgrounding",
14
14
  "--disable-features=CalculateNativeWinOcclusion"
15
15
  ];
16
+ export const DEFAULT_REQUIRED_CHROME_FLAGS = LID_CLOSED_SAFE_CHROME_ARGS;
16
17
 
17
18
  export const ALLOWED_CDP_DOMAINS = new Set([
18
19
  "Accessibility",
@@ -625,6 +626,109 @@ function parseExtraChromeArgs(value = "") {
625
626
  .filter(Boolean);
626
627
  }
627
628
 
629
+ export function parseChromeCommandLineArgs(commandLineOrArgs = []) {
630
+ if (Array.isArray(commandLineOrArgs)) {
631
+ return commandLineOrArgs
632
+ .map((item) => String(item || "").trim())
633
+ .filter(Boolean);
634
+ }
635
+
636
+ const text = String(commandLineOrArgs || "").trim();
637
+ if (!text) return [];
638
+ const args = [];
639
+ let current = "";
640
+ let quote = null;
641
+ for (const char of text) {
642
+ if (quote) {
643
+ if (char === quote) {
644
+ quote = null;
645
+ } else {
646
+ current += char;
647
+ }
648
+ continue;
649
+ }
650
+ if (char === '"' || char === "'") {
651
+ quote = char;
652
+ continue;
653
+ }
654
+ if (/\s/.test(char)) {
655
+ if (current) {
656
+ args.push(current);
657
+ current = "";
658
+ }
659
+ continue;
660
+ }
661
+ current += char;
662
+ }
663
+ if (current) args.push(current);
664
+ return args;
665
+ }
666
+
667
+ function splitChromeFeatureList(value = "") {
668
+ return String(value || "")
669
+ .split(",")
670
+ .map((item) => item.trim())
671
+ .filter(Boolean);
672
+ }
673
+
674
+ function chromeFlagIsPresent(args, requiredFlag) {
675
+ if (!requiredFlag) return true;
676
+ const disableFeaturesPrefix = "--disable-features=";
677
+ if (requiredFlag.startsWith(disableFeaturesPrefix)) {
678
+ const requiredFeatures = splitChromeFeatureList(requiredFlag.slice(disableFeaturesPrefix.length));
679
+ const disableFeatureArgs = args.filter((arg) => arg.startsWith(disableFeaturesPrefix));
680
+ const lastDisableFeatureArg = disableFeatureArgs[disableFeatureArgs.length - 1] || "";
681
+ const features = splitChromeFeatureList(lastDisableFeatureArg.slice(disableFeaturesPrefix.length));
682
+ return requiredFeatures.every((feature) => features.includes(feature));
683
+ }
684
+ if (args.includes(requiredFlag)) return true;
685
+ return false;
686
+ }
687
+
688
+ export function getMissingRequiredChromeFlags(
689
+ commandLineOrArgs = [],
690
+ requiredFlags = DEFAULT_REQUIRED_CHROME_FLAGS
691
+ ) {
692
+ const args = parseChromeCommandLineArgs(commandLineOrArgs);
693
+ return requiredFlags.filter((flag) => !chromeFlagIsPresent(args, flag));
694
+ }
695
+
696
+ function normalizeChromeLaunchArgs(args = []) {
697
+ const disableFeaturesPrefix = "--disable-features=";
698
+ const result = [];
699
+ const seen = new Set();
700
+ const disabledFeatures = [];
701
+ const disabledFeatureSet = new Set();
702
+ let disabledFeatureIndex = -1;
703
+
704
+ for (const rawArg of args) {
705
+ const arg = String(rawArg || "").trim();
706
+ if (!arg) continue;
707
+ if (arg.startsWith(disableFeaturesPrefix)) {
708
+ if (disabledFeatureIndex < 0) {
709
+ disabledFeatureIndex = result.length;
710
+ result.push(null);
711
+ }
712
+ for (const feature of splitChromeFeatureList(arg.slice(disableFeaturesPrefix.length))) {
713
+ if (!disabledFeatureSet.has(feature)) {
714
+ disabledFeatureSet.add(feature);
715
+ disabledFeatures.push(feature);
716
+ }
717
+ }
718
+ continue;
719
+ }
720
+ if (seen.has(arg)) continue;
721
+ seen.add(arg);
722
+ result.push(arg);
723
+ }
724
+
725
+ return result.map((arg) => (
726
+ arg === null
727
+ ? `${disableFeaturesPrefix}${disabledFeatures.join(",")}`
728
+ : arg
729
+ ));
730
+ }
731
+
628
732
  export function buildBossChromeLaunchArgs({
629
733
  port = DEFAULT_CHROME_PORT,
630
734
  userDataDir = "",
@@ -642,7 +746,356 @@ export function buildBossChromeLaunchArgs({
642
746
  "--new-window",
643
747
  url
644
748
  ];
645
- return Array.from(new Set(args.filter(Boolean)));
749
+ return normalizeChromeLaunchArgs(args);
750
+ }
751
+
752
+ function execFileText(file, args = [], { timeoutMs = 5000, maxBuffer = 1024 * 1024 } = {}) {
753
+ return new Promise((resolve) => {
754
+ execFile(file, args, {
755
+ timeout: timeoutMs,
756
+ maxBuffer,
757
+ windowsHide: true
758
+ }, (error, stdout, stderr) => {
759
+ resolve({
760
+ ok: !error,
761
+ stdout: String(stdout || ""),
762
+ stderr: String(stderr || ""),
763
+ error: error?.message || ""
764
+ });
765
+ });
766
+ });
767
+ }
768
+
769
+ async function inspectChromeCommandLineViaCdp({
770
+ host = DEFAULT_CHROME_HOST,
771
+ port = DEFAULT_CHROME_PORT
772
+ } = {}) {
773
+ let client = null;
774
+ try {
775
+ client = await CDP({ host, port });
776
+ const result = await client.Browser.getBrowserCommandLine();
777
+ const args = parseChromeCommandLineArgs(result?.arguments || result?.commandLine || result?.command_line || []);
778
+ if (args.length === 0) {
779
+ return {
780
+ ok: false,
781
+ source: "cdp_browser_command_line",
782
+ arguments: [],
783
+ error: "Browser.getBrowserCommandLine returned no command-line arguments"
784
+ };
785
+ }
786
+ return {
787
+ ok: true,
788
+ source: "cdp_browser_command_line",
789
+ arguments: args
790
+ };
791
+ } catch (error) {
792
+ return {
793
+ ok: false,
794
+ source: "cdp_browser_command_line",
795
+ arguments: [],
796
+ error: error?.message || String(error || "")
797
+ };
798
+ } finally {
799
+ if (client) {
800
+ await client.close().catch(() => {});
801
+ }
802
+ }
803
+ }
804
+
805
+ function parseWindowsProcessListJson(text = "") {
806
+ const trimmed = String(text || "").trim();
807
+ if (!trimmed) return [];
808
+ const parsed = JSON.parse(trimmed);
809
+ const items = Array.isArray(parsed) ? parsed : [parsed];
810
+ return items
811
+ .map((item) => ({
812
+ pid: Number(item?.ProcessId),
813
+ command_line: String(item?.CommandLine || "")
814
+ }))
815
+ .filter((item) => Number.isFinite(item.pid) && item.command_line);
816
+ }
817
+
818
+ function parsePosixProcessList(text = "", port = DEFAULT_CHROME_PORT) {
819
+ const portPattern = new RegExp(`--remote-debugging-port(?:=|\\s+)${port}(?=\\s|$)`);
820
+ return String(text || "")
821
+ .split(/\r?\n/)
822
+ .map((line) => {
823
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
824
+ return match
825
+ ? { pid: Number(match[1]), command_line: match[2] }
826
+ : null;
827
+ })
828
+ .filter((item) => item && Number.isFinite(item.pid) && portPattern.test(item.command_line));
829
+ }
830
+
831
+ function summarizeChromeProcesses(processes = []) {
832
+ return processes
833
+ .map((item) => ({
834
+ pid: item.pid,
835
+ command_line_length: String(item.command_line || "").length
836
+ }))
837
+ .filter((item) => Number.isFinite(item.pid));
838
+ }
839
+
840
+ async function inspectChromeCommandLineViaProcessList({
841
+ port = DEFAULT_CHROME_PORT
842
+ } = {}) {
843
+ const portText = String(port);
844
+ let processes = [];
845
+ let raw = null;
846
+
847
+ if (process.platform === "win32") {
848
+ const portPattern = `--remote-debugging-port(=|\\s+)${portText}(\\s|$)`;
849
+ const script = [
850
+ "$items = Get-CimInstance Win32_Process",
851
+ `| Where-Object { $_.CommandLine -and $_.CommandLine -match '${portPattern}' }`,
852
+ "| Select-Object ProcessId,CommandLine;",
853
+ "$items | ConvertTo-Json -Compress"
854
+ ].join(" ");
855
+ raw = await execFileText("powershell.exe", [
856
+ "-NoProfile",
857
+ "-ExecutionPolicy",
858
+ "Bypass",
859
+ "-Command",
860
+ script
861
+ ], { timeoutMs: 6000 });
862
+ if (!raw.ok) {
863
+ return {
864
+ ok: false,
865
+ source: "process_list",
866
+ arguments: [],
867
+ processes: [],
868
+ error: raw.error || raw.stderr || "Failed to inspect Windows process list"
869
+ };
870
+ }
871
+ try {
872
+ processes = parseWindowsProcessListJson(raw.stdout);
873
+ } catch (error) {
874
+ return {
875
+ ok: false,
876
+ source: "process_list",
877
+ arguments: [],
878
+ processes: [],
879
+ error: `Failed to parse Windows process list: ${error?.message || error}`
880
+ };
881
+ }
882
+ } else {
883
+ const psArgs = process.platform === "darwin"
884
+ ? ["-axo", "pid=,command="]
885
+ : ["-eo", "pid=,args="];
886
+ raw = await execFileText("ps", psArgs, { timeoutMs: 6000 });
887
+ if (!raw.ok) {
888
+ return {
889
+ ok: false,
890
+ source: "process_list",
891
+ arguments: [],
892
+ processes: [],
893
+ error: raw.error || raw.stderr || "Failed to inspect process list"
894
+ };
895
+ }
896
+ processes = parsePosixProcessList(raw.stdout, port);
897
+ }
898
+
899
+ if (processes.length === 0) {
900
+ return {
901
+ ok: false,
902
+ source: "process_list",
903
+ arguments: [],
904
+ processes: [],
905
+ error: `No local process was found for --remote-debugging-port=${port}`
906
+ };
907
+ }
908
+ const primary = processes[0];
909
+ return {
910
+ ok: true,
911
+ source: "process_list",
912
+ arguments: parseChromeCommandLineArgs(primary.command_line),
913
+ process: {
914
+ pid: primary.pid,
915
+ command_line_length: primary.command_line.length
916
+ },
917
+ processes: summarizeChromeProcesses(processes)
918
+ };
919
+ }
920
+
921
+ export async function inspectChromeDebugCommandLine({
922
+ host = DEFAULT_CHROME_HOST,
923
+ port = DEFAULT_CHROME_PORT,
924
+ _deps = {}
925
+ } = {}) {
926
+ const inspectViaCdp = _deps.inspectChromeCommandLineViaCdpImpl || inspectChromeCommandLineViaCdp;
927
+ const inspectViaProcess = _deps.inspectChromeCommandLineViaProcessListImpl || inspectChromeCommandLineViaProcessList;
928
+ const cdpResult = await inspectViaCdp({ host, port });
929
+ if (cdpResult?.ok && cdpResult.arguments?.length) {
930
+ return cdpResult;
931
+ }
932
+ if (!isLocalChromeHost(host)) {
933
+ return {
934
+ ok: false,
935
+ source: cdpResult?.source || "unknown",
936
+ arguments: [],
937
+ error: cdpResult?.error || `Cannot inspect process list for non-local Chrome debug host: ${host}`
938
+ };
939
+ }
940
+ const processResult = await inspectViaProcess({ port });
941
+ if (processResult?.ok && processResult.arguments?.length) {
942
+ return {
943
+ ...processResult,
944
+ cdp_error: cdpResult?.error || null
945
+ };
946
+ }
947
+ return {
948
+ ok: false,
949
+ source: processResult?.source || cdpResult?.source || "unknown",
950
+ arguments: [],
951
+ processes: processResult?.processes || [],
952
+ error: processResult?.error || cdpResult?.error || "Chrome command line could not be inspected"
953
+ };
954
+ }
955
+
956
+ async function waitForChromeDebugPortClosed({
957
+ host = DEFAULT_CHROME_HOST,
958
+ port = DEFAULT_CHROME_PORT,
959
+ timeoutMs = 6000,
960
+ intervalMs = 300,
961
+ listChromeTargetsImpl = listChromeTargets
962
+ } = {}) {
963
+ const started = Date.now();
964
+ let lastError = null;
965
+ let lastTargetCount = 0;
966
+ while (Date.now() - started <= timeoutMs) {
967
+ try {
968
+ const targets = await listChromeTargetsImpl({ host, port });
969
+ lastTargetCount = Array.isArray(targets) ? targets.length : 0;
970
+ } catch (error) {
971
+ if (isChromeDebugUnavailableError(error)) {
972
+ return {
973
+ ok: true,
974
+ elapsed_ms: Date.now() - started
975
+ };
976
+ }
977
+ lastError = error;
978
+ }
979
+ await sleep(intervalMs);
980
+ }
981
+ return {
982
+ ok: false,
983
+ elapsed_ms: Date.now() - started,
984
+ target_count: lastTargetCount,
985
+ error: lastError?.message || `Chrome debug port ${port} is still reachable`
986
+ };
987
+ }
988
+
989
+ export async function closeChromeDebugInstance({
990
+ host = DEFAULT_CHROME_HOST,
991
+ port = DEFAULT_CHROME_PORT,
992
+ processes = [],
993
+ timeoutMs = 8000,
994
+ intervalMs = 300,
995
+ _deps = {}
996
+ } = {}) {
997
+ if (!isLocalChromeHost(host)) {
998
+ return {
999
+ ok: false,
1000
+ method: "none",
1001
+ error: `Refusing to close non-local Chrome debug host: ${host}`
1002
+ };
1003
+ }
1004
+
1005
+ const listChromeTargetsImpl = _deps.listChromeTargetsImpl || listChromeTargets;
1006
+ const waitClosed = _deps.waitForChromeDebugPortClosedImpl || waitForChromeDebugPortClosed;
1007
+ let browserCloseAttempted = false;
1008
+ let browserCloseError = null;
1009
+ try {
1010
+ let client = null;
1011
+ try {
1012
+ client = await CDP({ host, port });
1013
+ if (typeof client?.Browser?.close !== "function") {
1014
+ throw new Error("Browser.close is not available");
1015
+ }
1016
+ browserCloseAttempted = true;
1017
+ await client.Browser.close();
1018
+ } finally {
1019
+ if (client) await client.close().catch(() => {});
1020
+ }
1021
+ } catch (error) {
1022
+ browserCloseError = error?.message || String(error || "");
1023
+ }
1024
+
1025
+ let closed = await waitClosed({ host, port, timeoutMs, intervalMs, listChromeTargetsImpl });
1026
+ if (closed.ok) {
1027
+ return {
1028
+ ok: true,
1029
+ method: browserCloseAttempted ? "Browser.close" : "port_already_closed",
1030
+ elapsed_ms: closed.elapsed_ms,
1031
+ browser_close_error: browserCloseError
1032
+ };
1033
+ }
1034
+
1035
+ const pids = Array.from(new Set((processes || [])
1036
+ .map((item) => Number(item?.pid))
1037
+ .filter((pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid)));
1038
+ const killedPids = [];
1039
+ const processErrors = [];
1040
+ for (const pid of pids) {
1041
+ try {
1042
+ process.kill(pid);
1043
+ killedPids.push(pid);
1044
+ } catch (error) {
1045
+ processErrors.push({
1046
+ pid,
1047
+ error: error?.message || String(error || "")
1048
+ });
1049
+ }
1050
+ }
1051
+
1052
+ if (killedPids.length > 0) {
1053
+ closed = await waitClosed({ host, port, timeoutMs, intervalMs, listChromeTargetsImpl });
1054
+ if (closed.ok) {
1055
+ return {
1056
+ ok: true,
1057
+ method: browserCloseAttempted ? "Browser.close+process.kill" : "process.kill",
1058
+ elapsed_ms: closed.elapsed_ms,
1059
+ killed_pids: killedPids,
1060
+ browser_close_error: browserCloseError,
1061
+ process_errors: processErrors
1062
+ };
1063
+ }
1064
+ }
1065
+
1066
+ return {
1067
+ ok: false,
1068
+ method: browserCloseAttempted && killedPids.length > 0
1069
+ ? "Browser.close+process.kill"
1070
+ : browserCloseAttempted
1071
+ ? "Browser.close"
1072
+ : killedPids.length > 0
1073
+ ? "process.kill"
1074
+ : "none",
1075
+ killed_pids: killedPids,
1076
+ browser_close_error: browserCloseError,
1077
+ process_errors: processErrors,
1078
+ wait: closed,
1079
+ error: closed.error || browserCloseError || "Failed to close Chrome debug instance"
1080
+ };
1081
+ }
1082
+
1083
+ function summarizeRelaunch(result = {}, reason = "") {
1084
+ return {
1085
+ reason,
1086
+ launched: Boolean(result?.launched),
1087
+ chrome_path: result?.chrome_path || null,
1088
+ user_data_dir: result?.user_data_dir || null,
1089
+ launch_args: Array.isArray(result?.launch_args) ? result.launch_args : [],
1090
+ readiness: result?.readiness || null
1091
+ };
1092
+ }
1093
+
1094
+ function createChromeGuardError(message, code, chromeGuard) {
1095
+ const error = new Error(message);
1096
+ error.code = code;
1097
+ error.chrome_guard = chromeGuard;
1098
+ return error;
646
1099
  }
647
1100
 
648
1101
  export async function waitForChromeDebugPort({
@@ -677,7 +1130,8 @@ export async function launchChromeDebugInstance({
677
1130
  host = DEFAULT_CHROME_HOST,
678
1131
  port = DEFAULT_CHROME_PORT,
679
1132
  url = "about:blank",
680
- slowLive = false
1133
+ slowLive = false,
1134
+ userDataDir = ""
681
1135
  } = {}) {
682
1136
  if (!isLocalChromeHost(host)) {
683
1137
  throw new Error(`Cannot auto-launch Chrome for non-local debug host: ${host}`);
@@ -686,8 +1140,9 @@ export async function launchChromeDebugInstance({
686
1140
  if (!chromePath) {
687
1141
  throw new Error("Chrome executable not found. Set BOSS_MCP_CHROME_PATH or BOSS_RECOMMEND_CHROME_PATH.");
688
1142
  }
689
- const userDataDir = getBossChromeUserDataDir(port);
690
- const args = buildBossChromeLaunchArgs({ port, userDataDir, url });
1143
+ const resolvedUserDataDir = userDataDir || getBossChromeUserDataDir(port);
1144
+ ensureDir(resolvedUserDataDir);
1145
+ const args = buildBossChromeLaunchArgs({ port, userDataDir: resolvedUserDataDir, url });
691
1146
  const child = spawn(chromePath, args, {
692
1147
  detached: true,
693
1148
  stdio: "ignore",
@@ -706,7 +1161,7 @@ export async function launchChromeDebugInstance({
706
1161
  return {
707
1162
  launched: true,
708
1163
  chrome_path: chromePath,
709
- user_data_dir: userDataDir,
1164
+ user_data_dir: resolvedUserDataDir,
710
1165
  launch_args: args,
711
1166
  port,
712
1167
  url,
@@ -722,21 +1177,168 @@ export async function ensureChromeDebugPort({
722
1177
  port = DEFAULT_CHROME_PORT,
723
1178
  url = "about:blank",
724
1179
  slowLive = false,
725
- launchIfMissing = true
1180
+ launchIfMissing = true,
1181
+ userDataDir = "",
1182
+ enforceRequiredFlags = true,
1183
+ requiredFlags = DEFAULT_REQUIRED_CHROME_FLAGS,
1184
+ _deps = {}
726
1185
  } = {}) {
1186
+ const listChromeTargetsImpl = _deps.listChromeTargetsImpl || listChromeTargets;
1187
+ const inspectCommandLineImpl = _deps.inspectChromeDebugCommandLineImpl || inspectChromeDebugCommandLine;
1188
+ const closeChromeDebugInstanceImpl = _deps.closeChromeDebugInstanceImpl || closeChromeDebugInstance;
1189
+ const launchChromeDebugInstanceImpl = _deps.launchChromeDebugInstanceImpl || launchChromeDebugInstance;
1190
+ const required = Array.from(new Set((requiredFlags || []).filter(Boolean)));
1191
+ const baseGuard = {
1192
+ guard_checked: Boolean(enforceRequiredFlags),
1193
+ required_flags: required,
1194
+ missing_flags: [],
1195
+ required_flags_ok: !enforceRequiredFlags,
1196
+ replaced: false,
1197
+ close_method: null,
1198
+ relaunch: null,
1199
+ host,
1200
+ port
1201
+ };
1202
+
727
1203
  try {
728
- const targets = await listChromeTargets({ host, port });
729
- return {
730
- launched: false,
731
- reused: true,
732
- port,
733
- target_count: targets.length
1204
+ const targets = await listChromeTargetsImpl({ host, port });
1205
+ if (!enforceRequiredFlags) {
1206
+ return {
1207
+ launched: false,
1208
+ reused: true,
1209
+ port,
1210
+ target_count: targets.length,
1211
+ ...baseGuard
1212
+ };
1213
+ }
1214
+
1215
+ const commandLine = await inspectCommandLineImpl({ host, port, _deps });
1216
+ const missingFlags = commandLine?.ok
1217
+ ? getMissingRequiredChromeFlags(commandLine.arguments, required)
1218
+ : required.slice();
1219
+ const commandLineEvidence = {
1220
+ command_line_source: commandLine?.source || "unknown",
1221
+ command_line_error: commandLine?.ok ? null : (commandLine?.error || "Chrome command line could not be inspected"),
1222
+ command_line_args_count: Array.isArray(commandLine?.arguments) ? commandLine.arguments.length : 0,
1223
+ inspected_process: commandLine?.process || null,
1224
+ inspected_processes: commandLine?.processes || []
734
1225
  };
1226
+ if (missingFlags.length === 0) {
1227
+ return {
1228
+ launched: false,
1229
+ reused: true,
1230
+ port,
1231
+ target_count: targets.length,
1232
+ ...baseGuard,
1233
+ required_flags_ok: true,
1234
+ ...commandLineEvidence
1235
+ };
1236
+ }
1237
+
1238
+ const guard = {
1239
+ ...baseGuard,
1240
+ required_flags_ok: false,
1241
+ missing_flags: missingFlags,
1242
+ target_count: targets.length,
1243
+ ...commandLineEvidence
1244
+ };
1245
+ if (!isLocalChromeHost(host)) {
1246
+ throw createChromeGuardError(
1247
+ `Chrome debug host ${host}:${port} is missing required Chrome flags and is not local, so it will not be auto-closed.`,
1248
+ "CHROME_REQUIRED_FLAGS_MISSING_NON_LOCAL",
1249
+ guard
1250
+ );
1251
+ }
1252
+
1253
+ const closeResult = await closeChromeDebugInstanceImpl({
1254
+ host,
1255
+ port,
1256
+ processes: commandLine?.processes || [],
1257
+ _deps
1258
+ });
1259
+ if (!closeResult?.ok) {
1260
+ throw createChromeGuardError(
1261
+ `Chrome debug instance on port ${port} is missing required flags and could not be closed: ${closeResult?.error || "unknown close failure"}`,
1262
+ "CHROME_REQUIRED_FLAGS_REPLACE_FAILED",
1263
+ {
1264
+ ...guard,
1265
+ close_method: closeResult?.method || null,
1266
+ close_result: closeResult || null
1267
+ }
1268
+ );
1269
+ }
1270
+
1271
+ try {
1272
+ const relaunch = await launchChromeDebugInstanceImpl({
1273
+ host,
1274
+ port,
1275
+ url,
1276
+ slowLive,
1277
+ userDataDir
1278
+ });
1279
+ return {
1280
+ ...relaunch,
1281
+ reused: false,
1282
+ ...guard,
1283
+ required_flags_ok: true,
1284
+ replaced: true,
1285
+ close_method: closeResult.method || null,
1286
+ close_result: closeResult,
1287
+ relaunch: summarizeRelaunch(relaunch, "missing_required_flags")
1288
+ };
1289
+ } catch (error) {
1290
+ throw createChromeGuardError(
1291
+ `Chrome debug instance on port ${port} was closed for missing flags, but relaunch failed: ${error?.message || error}`,
1292
+ "CHROME_REQUIRED_FLAGS_RELAUNCH_FAILED",
1293
+ {
1294
+ ...guard,
1295
+ close_method: closeResult.method || null,
1296
+ close_result: closeResult,
1297
+ relaunch: {
1298
+ reason: "missing_required_flags",
1299
+ launched: false,
1300
+ error: error?.message || String(error || "")
1301
+ }
1302
+ }
1303
+ );
1304
+ }
735
1305
  } catch (error) {
1306
+ if (error?.chrome_guard) {
1307
+ throw error;
1308
+ }
736
1309
  if (!launchIfMissing || !isChromeDebugUnavailableError(error)) {
737
1310
  throw error;
738
1311
  }
739
- return launchChromeDebugInstance({ host, port, url, slowLive });
1312
+ try {
1313
+ const relaunch = await launchChromeDebugInstanceImpl({
1314
+ host,
1315
+ port,
1316
+ url,
1317
+ slowLive,
1318
+ userDataDir
1319
+ });
1320
+ return {
1321
+ ...baseGuard,
1322
+ ...relaunch,
1323
+ reused: false,
1324
+ required_flags_ok: true,
1325
+ relaunch: summarizeRelaunch(relaunch, "port_unreachable")
1326
+ };
1327
+ } catch (launchError) {
1328
+ throw createChromeGuardError(
1329
+ `Chrome debug port ${port} was unreachable and Chrome relaunch failed: ${launchError?.message || launchError}`,
1330
+ "CHROME_RELAUNCH_FAILED",
1331
+ {
1332
+ ...baseGuard,
1333
+ required_flags_ok: false,
1334
+ relaunch: {
1335
+ reason: "port_unreachable",
1336
+ launched: false,
1337
+ error: launchError?.message || String(launchError || "")
1338
+ }
1339
+ }
1340
+ );
1341
+ }
740
1342
  }
741
1343
  }
742
1344
 
@@ -784,21 +1386,25 @@ export async function connectToChromeTargetOrOpen({
784
1386
  targetUrl,
785
1387
  allowNavigate = true,
786
1388
  slowLive = false,
787
- launchIfMissing = true
1389
+ launchIfMissing = true,
1390
+ _deps = {}
788
1391
  } = {}) {
1392
+ const ensureChromeDebugPortImpl = _deps.ensureChromeDebugPortImpl || ensureChromeDebugPort;
1393
+ const connectToChromeTargetImpl = _deps.connectToChromeTargetImpl || connectToChromeTarget;
1394
+ const openChromeTargetImpl = _deps.openChromeTargetImpl || openChromeTarget;
789
1395
  let chrome = null;
790
- if (allowNavigate && targetUrl) {
791
- chrome = await ensureChromeDebugPort({
1396
+ if (targetUrl) {
1397
+ chrome = await ensureChromeDebugPortImpl({
792
1398
  host,
793
1399
  port,
794
1400
  url: targetUrl,
795
1401
  slowLive,
796
- launchIfMissing
1402
+ launchIfMissing: allowNavigate && launchIfMissing
797
1403
  });
798
1404
  }
799
1405
 
800
1406
  try {
801
- const session = await connectToChromeTarget({
1407
+ const session = await connectToChromeTargetImpl({
802
1408
  host,
803
1409
  port,
804
1410
  targetUrlIncludes,
@@ -816,7 +1422,7 @@ export async function connectToChromeTargetOrOpen({
816
1422
 
817
1423
  if (typeof fallbackTargetPredicate === "function") {
818
1424
  try {
819
- const session = await connectToChromeTarget({
1425
+ const session = await connectToChromeTargetImpl({
820
1426
  host,
821
1427
  port,
822
1428
  targetPredicate: fallbackTargetPredicate
@@ -834,9 +1440,9 @@ export async function connectToChromeTargetOrOpen({
834
1440
 
835
1441
  let openAttempt = null;
836
1442
  if (targetUrl) {
837
- openAttempt = await openChromeTarget({ host, port, url: targetUrl });
1443
+ openAttempt = await openChromeTargetImpl({ host, port, url: targetUrl });
838
1444
  if (openAttempt.ok) {
839
- const session = await connectToChromeTarget({
1445
+ const session = await connectToChromeTargetImpl({
840
1446
  host,
841
1447
  port,
842
1448
  targetPredicate: (target) => (
@@ -856,7 +1462,7 @@ export async function connectToChromeTargetOrOpen({
856
1462
  }
857
1463
  }
858
1464
 
859
- const session = await connectToChromeTarget({
1465
+ const session = await connectToChromeTargetImpl({
860
1466
  host,
861
1467
  port,
862
1468
  targetPredicate: (target) => target?.type === "page"