@reconcrap/boss-recommend-mcp 2.0.50 → 2.0.51

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.0.50",
3
+ "version": "2.0.51",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
package/src/cli.js CHANGED
@@ -7,10 +7,10 @@ import { createRequire } from "node:module";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import {
9
9
  assertNoForbiddenCdpCalls,
10
- buildBossChromeLaunchArgs,
11
10
  bringPageToFront,
12
11
  connectToChromeTarget,
13
12
  enableDomains,
13
+ ensureChromeDebugPort,
14
14
  getDocumentRoot,
15
15
  querySelector,
16
16
  sleep as sleepMs
@@ -2209,51 +2209,38 @@ async function launchChrome(options = {}) {
2209
2209
  const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
2210
2210
  process.env.BOSS_RECOMMEND_CHROME_PORT = String(port);
2211
2211
  const timing = getLaunchChromeTiming(options);
2212
-
2213
- const initialState = await inspectBossRecommendPageStateCdp(port, {
2214
- timeoutMs: timing.initialTimeoutMs,
2215
- pollMs: timing.pollMs
2216
- });
2217
- if (initialState.state !== "DEBUG_PORT_UNREACHABLE") {
2218
- console.log(`Reusing existing Chrome debug instance on port ${port}`);
2219
- const pageState = await ensureBossRecommendPageReadyCdp(port, {
2220
- attempts: 2,
2221
- inspectTimeoutMs: timing.inspectTimeoutMs,
2222
- pollMs: timing.pollMs,
2223
- settleMs: timing.settleMs
2212
+ const userDataDir = getChromeUserDataDir(port);
2213
+ let chromeGuard = null;
2214
+ try {
2215
+ chromeGuard = await ensureChromeDebugPort({
2216
+ port,
2217
+ url: bossUrl,
2218
+ slowLive: Boolean(options["slow-live"] || options.slowLive),
2219
+ launchIfMissing: true,
2220
+ userDataDir
2224
2221
  });
2225
- if (pageState.ok) {
2226
- console.log("Boss recommend page is ready.");
2227
- const frontResult = await bringBossRecommendTabToFrontCdp(port);
2228
- if (frontResult.ok) {
2229
- console.log(`CDP methods: ${frontResult.method_log.join(", ") || "none"}`);
2230
- }
2231
- } else {
2232
- console.log(pageState.page_state?.message || "Boss recommend page is not ready.");
2222
+ } catch (error) {
2223
+ console.error(error?.message || String(error || "Chrome launch failed"));
2224
+ if (error?.chrome_guard) {
2225
+ console.error(JSON.stringify(error.chrome_guard, null, 2));
2233
2226
  }
2234
- return;
2235
- }
2236
-
2237
- const chromePath = getChromeExecutable();
2238
- if (!chromePath) {
2239
- console.error("Chrome executable not found. Set BOSS_RECOMMEND_CHROME_PATH or install Google Chrome.");
2240
2227
  process.exitCode = 1;
2241
2228
  return;
2242
2229
  }
2243
2230
 
2244
- const userDataDir = getChromeUserDataDir(port);
2245
- const args = buildBossChromeLaunchArgs({ port, userDataDir, url: bossUrl });
2246
- const child = spawn(chromePath, args, {
2247
- detached: true,
2248
- stdio: "ignore",
2249
- windowsHide: false
2250
- });
2251
- child.unref();
2252
- console.log(`Chrome launched with remote debugging port ${port}`);
2253
- console.log(`User data dir: ${userDataDir}`);
2254
- await sleepMs(timing.settleMs + 1200);
2231
+ if (chromeGuard.replaced) {
2232
+ console.log(`Replaced Chrome debug instance on port ${port} because required flags were missing: ${chromeGuard.missing_flags.join(", ")}`);
2233
+ } else if (chromeGuard.launched) {
2234
+ console.log(`Chrome launched with remote debugging port ${port}`);
2235
+ } else {
2236
+ console.log(`Reusing existing Chrome debug instance on port ${port} with required flags`);
2237
+ }
2238
+ console.log(`User data dir: ${chromeGuard.user_data_dir || userDataDir}`);
2239
+ if (chromeGuard.launched || chromeGuard.replaced) {
2240
+ await sleepMs(timing.settleMs + 1200);
2241
+ }
2255
2242
  const pageState = await ensureBossRecommendPageReadyCdp(port, {
2256
- attempts: 6,
2243
+ attempts: chromeGuard.launched || chromeGuard.replaced ? 6 : 2,
2257
2244
  inspectTimeoutMs: timing.inspectTimeoutMs,
2258
2245
  pollMs: timing.pollMs,
2259
2246
  settleMs: timing.settleMs
@@ -2382,15 +2369,30 @@ async function printDoctor(options = {}) {
2382
2369
  const configResolution = getBossScreenConfigResolution(workspaceRoot);
2383
2370
  const calibrationResolution = getFeaturedCalibrationResolutionLocal(workspaceRoot);
2384
2371
  const timing = getLaunchChromeTiming(options);
2372
+ const slowLive = Boolean(options["slow-live"] || options.slowLive);
2373
+ let chromeGuard = null;
2374
+ let chromeGuardError = null;
2375
+ try {
2376
+ chromeGuard = await ensureChromeDebugPort({
2377
+ port,
2378
+ url: bossUrl,
2379
+ slowLive,
2380
+ launchIfMissing: true,
2381
+ userDataDir: getChromeUserDataDir(port)
2382
+ });
2383
+ } catch (error) {
2384
+ chromeGuardError = error;
2385
+ chromeGuard = error?.chrome_guard || null;
2386
+ }
2385
2387
  let pageState = await inspectBossRecommendPageStateCdp(port, {
2386
- timeoutMs: options["slow-live"] || options.slowLive ? timing.initialTimeoutMs : 2000,
2387
- pollMs: options["slow-live"] || options.slowLive ? timing.pollMs : 500
2388
+ timeoutMs: slowLive ? timing.initialTimeoutMs : 2000,
2389
+ pollMs: slowLive ? timing.pollMs : 500
2388
2390
  });
2389
2391
  if (pageState.state === "RECOMMEND_READY") {
2390
2392
  pageState = await verifyRecommendPageStableCdp(port, {
2391
- settleMs: options["slow-live"] || options.slowLive ? timing.settleMs : 800,
2392
- recheckTimeoutMs: options["slow-live"] || options.slowLive ? timing.inspectTimeoutMs : 3000,
2393
- pollMs: options["slow-live"] || options.slowLive ? timing.pollMs : 500
2393
+ settleMs: slowLive ? timing.settleMs : 800,
2394
+ recheckTimeoutMs: slowLive ? timing.inspectTimeoutMs : 3000,
2395
+ pollMs: slowLive ? timing.pollMs : 500
2394
2396
  });
2395
2397
  }
2396
2398
  const resolvedConfigPath = configResolution.resolved_path || configResolution.writable_path;
@@ -2407,6 +2409,28 @@ async function printDoctor(options = {}) {
2407
2409
  ? `检测到配置文件(resolved_path):${resolvedConfigPath}`
2408
2410
  : "用户配置不存在(可通过 `boss-recommend-mcp init-config` 创建模板,或 `boss-recommend-mcp config set` 写入真实值)"
2409
2411
  });
2412
+ const requiredFlags = chromeGuard?.required_flags || [];
2413
+ const missingFlags = chromeGuard?.missing_flags || [];
2414
+ const chromeFlagsOk = Boolean(chromeGuard && !chromeGuardError && chromeGuard.required_flags_ok);
2415
+ checks.push({
2416
+ key: "chrome_required_flags",
2417
+ ok: chromeFlagsOk,
2418
+ path: `http://localhost:${port}`,
2419
+ required_flags: requiredFlags,
2420
+ missing_flags: missingFlags,
2421
+ replaced: Boolean(chromeGuard?.replaced),
2422
+ close_method: chromeGuard?.close_method || null,
2423
+ relaunch: chromeGuard?.relaunch || null,
2424
+ message: chromeFlagsOk
2425
+ ? chromeGuard?.replaced
2426
+ ? `Chrome 调试端口 ${port} 原实例缺少必需 flags,已自动关闭并用正确 flags 重新启动。`
2427
+ : chromeGuard?.launched
2428
+ ? `Chrome 调试端口 ${port} 已用必需 flags 启动。`
2429
+ : `Chrome 调试端口 ${port} 已确认包含必需 flags。`
2430
+ : chromeGuardError
2431
+ ? `Chrome 必需 flags 检查失败:${chromeGuardError.message}`
2432
+ : `Chrome 调试端口 ${port} 未确认包含必需 flags。`
2433
+ });
2410
2434
  checks.push({
2411
2435
  key: "chrome_debug_port",
2412
2436
  ok: pageState.state !== "DEBUG_PORT_UNREACHABLE",
@@ -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"