@reconcrap/boss-recruit-mcp 1.0.14 → 1.0.16

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/README.md CHANGED
@@ -25,10 +25,12 @@ npx @reconcrap/boss-recruit-mcp install
25
25
  - 安装 Codex skill 到 `$CODEX_HOME/skills/boss-recruit-pipeline`
26
26
  - 初始化用户配置到 `$CODEX_HOME/boss-recruit-mcp/screening-config.json`
27
27
  - 生成通用 MCP 配置模板到 `$CODEX_HOME/boss-recruit-mcp/agent-mcp-configs`
28
+ - 自动尝试写入已检测到的外部 agent MCP 配置(含 Trae / trae-cn / Cursor / Claude / OpenClaw)
29
+ - 自动尝试把 skill 镜像到已检测到的外部 agent skills 目录
28
30
  - 包内自带 `boss-search-cli` 与 `boss-screen-cli` 运行时文件,无需额外目录结构
29
31
  - 不包含 `favorite-calibration.json`,首次使用前需要自行校准生成
30
32
 
31
- ## 跨 Agent 快速接入(Cursor / Trae / Claude Code / OpenClaw)
33
+ ## 跨 Agent 快速接入(Cursor / Trae / trae-cn / Claude Code / OpenClaw)
32
34
 
33
35
  生成 MCP 配置模板:
34
36
 
@@ -58,6 +60,7 @@ $CODEX_HOME/boss-recruit-mcp/agent-mcp-configs
58
60
  boss-recruit-mcp mcp-config --client cursor
59
61
  boss-recruit-mcp mcp-config --client claudecode
60
62
  boss-recruit-mcp mcp-config --client trae
63
+ boss-recruit-mcp mcp-config --client trae-cn
61
64
  boss-recruit-mcp mcp-config --client openclaw
62
65
  boss-recruit-mcp mcp-config --client generic
63
66
  ```
@@ -73,6 +76,13 @@ boss-recruit-mcp mcp-config --client generic
73
76
  boss-recruit-mcp mcp-config --client generic --command boss-recruit-mcp --args-json "[\"start\"]"
74
77
  ```
75
78
 
79
+ 可选环境变量(用于跨 agent 自动配置):
80
+
81
+ ```bash
82
+ BOSS_RECRUIT_MCP_CONFIG_TARGETS # JSON 数组或系统 path 分隔路径列表,指定额外 mcp.json 目标文件
83
+ BOSS_RECRUIT_EXTERNAL_SKILL_DIRS # JSON 数组或系统 path 分隔路径列表,指定额外 skills 根目录
84
+ ```
85
+
76
86
  ## 准备配置
77
87
 
78
88
  1. 初始化后编辑用户配置文件:
@@ -218,11 +228,14 @@ boss-recruit-mcp doctor --port <port>
218
228
  - 正式执行前应先单独做一轮参数确认,把已识别参数、待确认项、缺失项、默认值风险分开给用户确认
219
229
  - 若用户没提“是否过滤近14天查看”,会在 `pending_questions` 里返回该问题,调用方应先补问再继续
220
230
  - 用户未补齐缺失参数时,只有在明确同意默认值及其质量风险后,才允许继续
221
- - `target_count` 表示“目标处理人数”,不是“目标通过人数”;状态一旦是 `COMPLETED`,就表示本轮已完成,不应因通过人数不足而自动重跑
222
- - 确认后自动执行:搜索 CLI -> 点击搜索 -> 勾选“过滤近14天查看”(如启用) -> 筛选 CLI
231
+ - `target_count` 表示“目标处理人数”,不是“目标通过人数”;会按“累计处理人数”自动多轮执行,直到达到目标处理人数,或新一轮搜索返回 0 个可筛选人选
232
+ - 若搜索页出现 `i.tip-nodata`(即使仍能看到候选人卡片),也会判定候选池已耗尽并结束多轮流程
233
+ - 第 2 轮及后续轮次会强制 `filter_recent_viewed=true`(即过滤近 14 天查看过的人选),不受首轮开关影响
234
+ - 确认后自动执行:搜索 CLI -> 点击搜索 -> 勾选“过滤近14天查看”(按轮次规则) -> 筛选 CLI
223
235
  - 返回摘要:目标数、已处理、通过数、耗时、输出 CSV
224
236
  - 执行前会先做本地依赖预检查,若目录 / 入口 / 配置文件缺失则返回 `PIPELINE_PREFLIGHT_FAILED`
225
237
  - 若缺少 `favorite-calibration.json`,会返回 `CALIBRATION_REQUIRED`
238
+ - 若某轮搜索返回可筛选候选人但筛选 `processed_count` 非法或为 0,会先导出当前累计 CSV,再返回 `SCREEN_NO_PROGRESS`
226
239
  - 若当前运行环境不允许启动子进程,会返回更明确的权限错误码而不是笼统失败
227
240
  - 配置文件查找顺序:`BOSS_RECRUIT_SCREEN_CONFIG` > 工作区 `boss-recruit-mcp/config/screening-config.json` > 用户目录 `$CODEX_HOME/boss-recruit-mcp/screening-config.json` > 包内示例配置
228
241
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recruit-mcp",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Unified MCP pipeline for boss-search-cli and boss-screen-cli",
5
5
  "keywords": [
6
6
  "boss",
@@ -20,6 +20,8 @@
20
20
  "install:local": "node src/cli.js install",
21
21
  "postinstall": "node scripts/postinstall.cjs",
22
22
  "test:parser": "node src/test-parser.js",
23
+ "test:pipeline": "node src/test-pipeline.js",
24
+ "test:protocol:win": "powershell -ExecutionPolicy Bypass -File scripts/verify-multi-round-protocol.ps1 -Mode unit",
23
25
  "test:regression:win": "powershell -ExecutionPolicy Bypass -File scripts/regression.ps1 -Mode quick"
24
26
  },
25
27
  "files": [
package/src/adapters.js CHANGED
@@ -3,6 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
6
+ import CDP from "chrome-remote-interface";
6
7
  const currentFilePath = fileURLToPath(import.meta.url);
7
8
  const packagedMcpDir = path.resolve(path.dirname(currentFilePath), "..");
8
9
  const bossSearchUrl = "https://www.zhipin.com/web/chat/search";
@@ -161,6 +162,82 @@ function parseSearchCount(output) {
161
162
  return Number.parseInt(m[1], 10);
162
163
  }
163
164
 
165
+ async function detectSearchNoDataTip(debugPort) {
166
+ let client = null;
167
+ try {
168
+ const targets = await CDP.List({ port: debugPort });
169
+ const target = targets.find(
170
+ (item) => typeof item?.url === "string" && item.url.includes("/web/chat/search")
171
+ ) || targets.find((item) => item?.type === "page");
172
+ if (!target) {
173
+ return {
174
+ ok: false,
175
+ exhausted: null,
176
+ error: "No page target found on Chrome DevTools"
177
+ };
178
+ }
179
+
180
+ client = await CDP({
181
+ port: debugPort,
182
+ target
183
+ });
184
+ const { Runtime } = client;
185
+ await Runtime.enable();
186
+
187
+ const expression = `(function () {
188
+ try {
189
+ var rootDoc = document;
190
+ var iframe = document.querySelector("iframe");
191
+ if (iframe && iframe.contentWindow && iframe.contentWindow.document) {
192
+ rootDoc = iframe.contentWindow.document;
193
+ }
194
+ var tip = rootDoc.querySelector("i.tip-nodata");
195
+ return {
196
+ exhausted: Boolean(tip),
197
+ selector: "i.tip-nodata"
198
+ };
199
+ } catch (err) {
200
+ return {
201
+ exhausted: false,
202
+ selector: "i.tip-nodata",
203
+ error: String(err && err.message ? err.message : err)
204
+ };
205
+ }
206
+ })()`;
207
+ const evaluated = await Runtime.evaluate({
208
+ expression,
209
+ returnByValue: true,
210
+ awaitPromise: true
211
+ });
212
+ if (evaluated.exceptionDetails) {
213
+ return {
214
+ ok: false,
215
+ exhausted: null,
216
+ error: evaluated.exceptionDetails.exception?.description || "Runtime.evaluate failed"
217
+ };
218
+ }
219
+
220
+ const value = evaluated.result?.value || {};
221
+ return {
222
+ ok: true,
223
+ exhausted: value.exhausted === true,
224
+ details: value
225
+ };
226
+ } catch (error) {
227
+ return {
228
+ ok: false,
229
+ exhausted: null,
230
+ error: error.message
231
+ };
232
+ } finally {
233
+ if (client) {
234
+ try {
235
+ await client.close();
236
+ } catch {}
237
+ }
238
+ }
239
+ }
240
+
164
241
  function parseScreenSummary(output) {
165
242
  const processed = output.match(/已处理:\s*(\d+)\s*人/);
166
243
  const passed = output.match(/通过筛选:\s*(\d+)\s*人/);
@@ -569,11 +646,18 @@ export async function runSearchCli({ workspaceRoot, searchParams }) {
569
646
 
570
647
  const combined = `${result.stdout}\n${result.stderr}`;
571
648
  const candidateCount = parseSearchCount(combined);
649
+ const tipCheck = result.code === 0
650
+ ? await detectSearchNoDataTip(debugPort)
651
+ : { ok: false, exhausted: null, error: null };
572
652
 
573
653
  return {
574
654
  ok: result.code === 0,
575
655
  exit_code: result.code,
576
656
  candidate_count: candidateCount,
657
+ no_data_tip_present: tipCheck.ok ? tipCheck.exhausted : null,
658
+ no_data_tip_check: tipCheck.ok
659
+ ? { ok: true, details: tipCheck.details || null }
660
+ : { ok: false, error: tipCheck.error || null },
577
661
  stdout: result.stdout,
578
662
  stderr: result.stderr,
579
663
  error_code: result.error_code || null
package/src/cli.js CHANGED
@@ -28,14 +28,16 @@ const SUPPORTED_MCP_CLIENTS = ["generic", "cursor", "trae", "claudecode", "openc
28
28
  const DEFAULT_MCP_SERVER_NAME = "boss-recruit";
29
29
  const DEFAULT_MCP_COMMAND = "npx";
30
30
  const DEFAULT_MCP_ARGS = ["-y", "@reconcrap/boss-recruit-mcp@latest", "start"];
31
- const AUTO_SYNC_SKIP_COMMANDS = new Set([
32
- "install",
33
- "install-skill",
34
- "where",
35
- "help",
36
- "--help",
37
- "-h"
38
- ]);
31
+ const AUTO_SYNC_SKIP_COMMANDS = new Set([
32
+ "install",
33
+ "install-skill",
34
+ "where",
35
+ "help",
36
+ "--help",
37
+ "-h"
38
+ ]);
39
+ const EXTERNAL_MCP_TARGETS_ENV = "BOSS_RECRUIT_MCP_CONFIG_TARGETS";
40
+ const EXTERNAL_SKILL_DIRS_ENV = "BOSS_RECRUIT_EXTERNAL_SKILL_DIRS";
39
41
 
40
42
  function getPackageVersion() {
41
43
  try {
@@ -58,13 +60,46 @@ function getCodexHome() {
58
60
  : path.join(os.homedir(), ".codex");
59
61
  }
60
62
 
61
- function ensureDir(targetPath) {
62
- fs.mkdirSync(targetPath, { recursive: true });
63
- }
64
-
65
- function getDesktopDir() {
66
- return path.join(os.homedir(), "Desktop");
67
- }
63
+ function ensureDir(targetPath) {
64
+ fs.mkdirSync(targetPath, { recursive: true });
65
+ }
66
+
67
+ function pathExists(targetPath) {
68
+ try {
69
+ return fs.existsSync(targetPath);
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ function readJsonObjectFileSafe(filePath) {
76
+ if (!pathExists(filePath)) return {};
77
+ try {
78
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
79
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
80
+ return parsed;
81
+ }
82
+ } catch {
83
+ // Fallback below.
84
+ }
85
+ return {};
86
+ }
87
+
88
+ function dedupePaths(items) {
89
+ const result = [];
90
+ const seen = new Set();
91
+ for (const item of items || []) {
92
+ const resolved = path.resolve(String(item || ""));
93
+ if (!resolved || seen.has(resolved)) continue;
94
+ seen.add(resolved);
95
+ result.push(resolved);
96
+ }
97
+ return result;
98
+ }
99
+
100
+ function getDesktopDir() {
101
+ return path.join(os.homedir(), "Desktop");
102
+ }
68
103
 
69
104
  function getUserConfigPath() {
70
105
  return path.join(getCodexHome(), "boss-recruit-mcp", "screening-config.json");
@@ -146,12 +181,13 @@ function parseStringArrayOption(value, label) {
146
181
  return parsed;
147
182
  }
148
183
 
149
- function normalizeMcpClientName(value) {
150
- const raw = String(value || "").trim().toLowerCase();
151
- if (!raw) return "";
152
- if (raw === "claude-code") return "claudecode";
153
- return raw;
154
- }
184
+ function normalizeMcpClientName(value) {
185
+ const raw = String(value || "").trim().toLowerCase();
186
+ if (!raw) return "";
187
+ if (raw === "claude-code") return "claudecode";
188
+ if (raw === "trae-cn") return "trae";
189
+ return raw;
190
+ }
155
191
 
156
192
  function parseMcpClientTargets(rawValue) {
157
193
  if (!rawValue) return SUPPORTED_MCP_CLIENTS.slice();
@@ -214,7 +250,7 @@ function buildMcpConfigFileContent(options = {}) {
214
250
  };
215
251
  }
216
252
 
217
- function writeMcpConfigFiles(options = {}) {
253
+ function writeMcpConfigFiles(options = {}) {
218
254
  const clients = parseMcpClientTargets(options.client);
219
255
  const outputDir = getAgentConfigOutputDir(options);
220
256
  ensureDir(outputDir);
@@ -227,8 +263,142 @@ function writeMcpConfigFiles(options = {}) {
227
263
  files.push({ client, file: filePath });
228
264
  }
229
265
 
230
- return { outputDir, files };
231
- }
266
+ return { outputDir, files };
267
+ }
268
+
269
+ function parsePathListFromEnv(raw) {
270
+ if (!raw) return [];
271
+ const text = String(raw).trim();
272
+ if (!text) return [];
273
+ try {
274
+ const parsed = JSON.parse(text);
275
+ if (Array.isArray(parsed)) {
276
+ return dedupePaths(parsed.filter(Boolean));
277
+ }
278
+ } catch {
279
+ // Fallback to delimiter split.
280
+ }
281
+ return dedupePaths(
282
+ text
283
+ .split(path.delimiter)
284
+ .map((item) => item.trim())
285
+ .filter(Boolean)
286
+ );
287
+ }
288
+
289
+ function getKnownExternalMcpConfigPaths() {
290
+ const home = os.homedir();
291
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
292
+ return dedupePaths([
293
+ path.join(appData, "Cursor", "User", "mcp.json"),
294
+ path.join(appData, "Trae", "User", "mcp.json"),
295
+ path.join(appData, "Trae CN", "User", "mcp.json"),
296
+ path.join(home, ".trae", "mcp.json"),
297
+ path.join(home, ".trae-cn", "mcp.json"),
298
+ path.join(home, ".claude", "mcp.json"),
299
+ path.join(home, ".openclaw", "mcp.json")
300
+ ]);
301
+ }
302
+
303
+ function resolveExternalMcpConfigTargets() {
304
+ const fromEnv = parsePathListFromEnv(process.env[EXTERNAL_MCP_TARGETS_ENV]);
305
+ const known = getKnownExternalMcpConfigPaths().filter((filePath) => {
306
+ if (pathExists(filePath)) return true;
307
+ return pathExists(path.dirname(filePath));
308
+ });
309
+ return dedupePaths([...fromEnv, ...known]);
310
+ }
311
+
312
+ function mergeMcpServerConfigFile(filePath, options = {}) {
313
+ const nextConfig = buildMcpConfigFileContent(options);
314
+ const serverName = Object.keys(nextConfig.mcpServers || {})[0] || DEFAULT_MCP_SERVER_NAME;
315
+ const launchConfig = nextConfig.mcpServers?.[serverName] || buildMcpLaunchConfig(options);
316
+ const current = readJsonObjectFileSafe(filePath);
317
+ const existingServers =
318
+ current?.mcpServers && typeof current.mcpServers === "object" && !Array.isArray(current.mcpServers)
319
+ ? current.mcpServers
320
+ : {};
321
+ const existingEntry = existingServers[serverName];
322
+ const merged = {
323
+ ...current,
324
+ mcpServers: {
325
+ ...existingServers,
326
+ [serverName]: launchConfig
327
+ }
328
+ };
329
+
330
+ ensureDir(path.dirname(filePath));
331
+ fs.writeFileSync(filePath, JSON.stringify(merged, null, 2), "utf8");
332
+ const updated = JSON.stringify(existingEntry || null) !== JSON.stringify(launchConfig);
333
+ return {
334
+ file: filePath,
335
+ server: serverName,
336
+ updated
337
+ };
338
+ }
339
+
340
+ function installExternalMcpConfigs(options = {}) {
341
+ const targets = resolveExternalMcpConfigTargets();
342
+ const applied = [];
343
+ const skipped = [];
344
+ for (const target of targets) {
345
+ try {
346
+ const existed = pathExists(target);
347
+ const merged = mergeMcpServerConfigFile(target, options);
348
+ applied.push({
349
+ file: target,
350
+ server: merged.server,
351
+ created: !existed,
352
+ updated: merged.updated
353
+ });
354
+ } catch (error) {
355
+ skipped.push({
356
+ file: target,
357
+ reason: error.message
358
+ });
359
+ }
360
+ }
361
+ return { targets, applied, skipped };
362
+ }
363
+
364
+ function getKnownExternalSkillBaseDirs() {
365
+ const home = os.homedir();
366
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
367
+ return dedupePaths([
368
+ path.join(home, ".cursor", "skills"),
369
+ path.join(home, ".trae", "skills"),
370
+ path.join(home, ".trae-cn", "skills"),
371
+ path.join(home, ".claude", "skills"),
372
+ path.join(home, ".openclaw", "skills"),
373
+ path.join(appData, "Cursor", "User", "skills"),
374
+ path.join(appData, "Trae", "User", "skills"),
375
+ path.join(appData, "Trae CN", "User", "skills"),
376
+ path.join(appData, "OpenClaw", "User", "skills")
377
+ ]);
378
+ }
379
+
380
+ function resolveExternalSkillBaseDirs() {
381
+ const fromEnv = parsePathListFromEnv(process.env[EXTERNAL_SKILL_DIRS_ENV]);
382
+ const known = getKnownExternalSkillBaseDirs().filter((dirPath) => pathExists(dirPath));
383
+ return dedupePaths([...fromEnv, ...known]);
384
+ }
385
+
386
+ function mirrorSkillToExternalDirs() {
387
+ const baseDirs = resolveExternalSkillBaseDirs();
388
+ const mirrored = [];
389
+ const skipped = [];
390
+ for (const baseDir of baseDirs) {
391
+ try {
392
+ const targetDir = path.join(baseDir, skillName);
393
+ ensureDir(path.dirname(targetDir));
394
+ fs.cpSync(skillSourceDir, targetDir, { recursive: true, force: true });
395
+ mirrored.push({ base_dir: baseDir, target_dir: targetDir });
396
+ } catch (error) {
397
+ skipped.push({ base_dir: baseDir, reason: error.message });
398
+ }
399
+ }
400
+ return { baseDirs, mirrored, skipped };
401
+ }
232
402
 
233
403
  function readTextFile(filePath, label) {
234
404
  const resolved = path.resolve(String(filePath));
@@ -909,7 +1079,7 @@ async function launchChrome(options) {
909
1079
  };
910
1080
  }
911
1081
 
912
- function printHelp() {
1082
+ function printHelp() {
913
1083
  console.log("boss-recruit-mcp");
914
1084
  console.log("");
915
1085
  console.log("Usage:");
@@ -920,7 +1090,7 @@ function printHelp() {
920
1090
  console.log(" boss-recruit-mcp install-skill Install only the Codex skill");
921
1091
  console.log(" boss-recruit-mcp init-config Create ~/.codex/boss-recruit-mcp/screening-config.json if missing");
922
1092
  console.log(" boss-recruit-mcp set-port Persist preferred Chrome debug port to active screening-config");
923
- console.log(" boss-recruit-mcp mcp-config Generate MCP config JSON for Cursor/Trae/Claude Code/OpenClaw");
1093
+ console.log(" boss-recruit-mcp mcp-config Generate MCP config JSON for Cursor/Trae(含 trae-cn)/Claude Code/OpenClaw");
924
1094
  console.log(" boss-recruit-mcp doctor Check config, calibration, and runtime prerequisites");
925
1095
  console.log(" boss-recruit-mcp calibrate Auto-open Boss search page, then run favorite-button calibration");
926
1096
  console.log(" boss-recruit-mcp launch-chrome Reuse existing Chrome debug instance when possible; otherwise launch one, open Boss search, and check login state");
@@ -972,22 +1142,41 @@ function printMcpConfig(options = {}) {
972
1142
  console.log("2. Merge its mcpServers block into that client's MCP config.");
973
1143
  }
974
1144
 
975
- function installAll() {
976
- const skillTarget = installSkill();
977
- const configResult = ensureUserConfig();
978
- const mcpTemplateResult = writeMcpConfigFiles({ client: "all" });
979
- console.log(`Skill installed to: ${skillTarget}`);
980
- if (configResult.created) {
981
- console.log(`Config template created at: ${configResult.path}`);
982
- } else {
983
- console.log(`Config already exists at: ${configResult.path}`);
1145
+ function installAll() {
1146
+ const skillTarget = installSkill();
1147
+ const configResult = ensureUserConfig();
1148
+ const mcpTemplateResult = writeMcpConfigFiles({ client: "all" });
1149
+ const externalMcpResult = installExternalMcpConfigs({});
1150
+ const externalSkillResult = mirrorSkillToExternalDirs();
1151
+ console.log(`Skill installed to: ${skillTarget}`);
1152
+ if (configResult.created) {
1153
+ console.log(`Config template created at: ${configResult.path}`);
1154
+ } else {
1155
+ console.log(`Config already exists at: ${configResult.path}`);
984
1156
  }
985
1157
  console.log(`MCP config templates exported to: ${mcpTemplateResult.outputDir}`);
986
- for (const item of mcpTemplateResult.files) {
987
- console.log(`- ${item.client}: ${item.file}`);
988
- }
989
- console.log("");
990
- console.log("Next steps:");
1158
+ for (const item of mcpTemplateResult.files) {
1159
+ console.log(`- ${item.client}: ${item.file}`);
1160
+ }
1161
+ if (externalMcpResult.targets.length > 0) {
1162
+ console.log(`Auto-configured external MCP files: ${externalMcpResult.applied.length}`);
1163
+ for (const item of externalMcpResult.applied) {
1164
+ const action = item.created ? "created" : item.updated ? "updated" : "unchanged";
1165
+ console.log(`- ${item.file} (${action})`);
1166
+ }
1167
+ } else {
1168
+ console.log("No external MCP config target detected. Set BOSS_RECRUIT_MCP_CONFIG_TARGETS to auto-configure custom agents.");
1169
+ }
1170
+ if (externalSkillResult.baseDirs.length > 0) {
1171
+ console.log(`Mirrored skill to external dirs: ${externalSkillResult.mirrored.length}`);
1172
+ for (const item of externalSkillResult.mirrored) {
1173
+ console.log(`- ${item.target_dir}`);
1174
+ }
1175
+ } else {
1176
+ console.log("No external skill dir detected. Set BOSS_RECRUIT_EXTERNAL_SKILL_DIRS to mirror skill for non-Codex agents.");
1177
+ }
1178
+ console.log("");
1179
+ console.log("Next steps:");
991
1180
  console.log("1. Fill in baseUrl/apiKey/model in the config file above.");
992
1181
  console.log("2. Choose a client template from the exported MCP config files and merge it into your AI client config.");
993
1182
  console.log("3. Choose a Chrome remote-debugging port (9222 is recommended, but you can reuse an existing port).");