@reconcrap/boss-recommend-mcp 1.3.38 → 2.0.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.
Files changed (85) hide show
  1. package/README.md +53 -33
  2. package/package.json +61 -9
  3. package/skills/boss-recommend-pipeline/SKILL.md +4 -0
  4. package/src/chat-mcp.js +1333 -0
  5. package/src/chat-runtime-config.js +559 -0
  6. package/src/cli.js +1095 -196
  7. package/src/core/browser/index.js +378 -0
  8. package/src/core/capture/index.js +298 -0
  9. package/src/core/cv-acquisition/index.js +219 -0
  10. package/src/core/greet-quota/index.js +54 -0
  11. package/src/core/infinite-list/index.js +459 -0
  12. package/src/core/reporting/legacy-csv.js +332 -0
  13. package/src/core/run/index.js +286 -0
  14. package/src/core/screening/index.js +1166 -0
  15. package/src/core/self-heal/index.js +848 -0
  16. package/src/domains/chat/cards.js +129 -0
  17. package/src/domains/chat/constants.js +183 -0
  18. package/src/domains/chat/detail.js +1369 -0
  19. package/src/domains/chat/index.js +7 -0
  20. package/src/domains/chat/jobs.js +334 -0
  21. package/src/domains/chat/page-guard.js +88 -0
  22. package/src/domains/chat/roots.js +56 -0
  23. package/src/domains/chat/run-service.js +1101 -0
  24. package/src/domains/recommend/actions.js +457 -0
  25. package/src/domains/recommend/cards.js +228 -0
  26. package/src/domains/recommend/constants.js +141 -0
  27. package/src/domains/recommend/detail.js +341 -0
  28. package/src/domains/recommend/filters.js +581 -0
  29. package/src/domains/recommend/index.js +10 -0
  30. package/src/domains/recommend/jobs.js +232 -0
  31. package/src/domains/recommend/refresh.js +204 -0
  32. package/src/domains/recommend/roots.js +78 -0
  33. package/src/domains/recommend/run-service.js +903 -0
  34. package/src/domains/recommend/scopes.js +245 -0
  35. package/src/domains/recruit/actions.js +277 -0
  36. package/src/domains/recruit/cards.js +67 -0
  37. package/src/domains/recruit/constants.js +130 -0
  38. package/src/domains/recruit/detail.js +414 -0
  39. package/src/domains/recruit/index.js +9 -0
  40. package/src/domains/recruit/instruction-parser.js +451 -0
  41. package/src/domains/recruit/refresh.js +40 -0
  42. package/src/domains/recruit/roots.js +68 -0
  43. package/src/domains/recruit/run-service.js +580 -0
  44. package/src/domains/recruit/search.js +1149 -0
  45. package/src/index.js +578 -419
  46. package/src/recommend-mcp.js +1257 -0
  47. package/src/recruit-mcp.js +1035 -0
  48. package/src/adapters.js +0 -3079
  49. package/src/boss-chat.js +0 -1037
  50. package/src/pipeline.js +0 -2249
  51. package/src/recommend-healing-config.js +0 -131
  52. package/src/recommend-healing-rules.json +0 -261
  53. package/src/self-heal.js +0 -2237
  54. package/src/test-adapters-runtime.js +0 -628
  55. package/src/test-boss-chat.js +0 -3196
  56. package/src/test-index-async.js +0 -498
  57. package/src/test-parser.js +0 -742
  58. package/src/test-pipeline.js +0 -2703
  59. package/src/test-run-state.js +0 -152
  60. package/src/test-self-heal.js +0 -224
  61. package/vendor/boss-chat-cli/README.md +0 -134
  62. package/vendor/boss-chat-cli/package.json +0 -53
  63. package/vendor/boss-chat-cli/src/app.js +0 -1501
  64. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  65. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  66. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  67. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  68. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  69. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  70. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  71. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  72. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  73. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  74. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  75. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  76. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  77. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  78. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  79. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  80. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -6927
  81. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  82. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  83. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2294
  84. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  85. package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
package/src/cli.js CHANGED
@@ -2,32 +2,33 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
- import { spawn } from "node:child_process";
5
+ import { spawn, spawnSync } from "node:child_process";
6
6
  import { createRequire } from "node:module";
7
7
  import { fileURLToPath } from "node:url";
8
- import { startServer } from "./index.js";
9
8
  import {
10
- ensureBossRecommendPageReady,
11
- getFeaturedCalibrationResolution,
12
- getScreenConfigResolution,
13
- inspectBossRecommendPageState,
14
- runRecommendCalibration,
15
- runPipelinePreflight,
16
- switchRecommendTab,
17
- waitRecommendFeaturedDetailReady
18
- } from "./adapters.js";
9
+ assertNoForbiddenCdpCalls,
10
+ bringPageToFront,
11
+ connectToChromeTarget,
12
+ enableDomains,
13
+ getDocumentRoot,
14
+ querySelector,
15
+ sleep as sleepMs
16
+ } from "./core/browser/index.js";
17
+ import {
18
+ bossChatHealthCheckTool,
19
+ cancelBossChatRunTool,
20
+ getBossChatRunTool,
21
+ pauseBossChatRunTool,
22
+ prepareBossChatRunTool,
23
+ resumeBossChatRunTool
24
+ } from "./chat-mcp.js";
25
+ import { listRecommendJobsTool } from "./recommend-mcp.js";
19
26
  import {
20
- cancelBossChatRun,
21
- ensureBossChatRuntimeReady,
22
- getBossChatHealthCheck,
23
- getBossChatRun,
24
- pauseBossChatRun,
25
- prepareBossChatRun,
26
- resolveBossChatRuntimeLayout,
27
- resumeBossChatRun,
28
- startBossChatRun
29
- } from "./boss-chat.js";
30
- import { runRecommendPipeline } from "./pipeline.js";
27
+ getBossScreenConfigResolution,
28
+ resolveBossChatRuntimeLayout as resolveCdpBossChatRuntimeLayout,
29
+ resolveBossScreeningConfig
30
+ } from "./chat-runtime-config.js";
31
+ import { startServer } from "./index.js";
31
32
 
32
33
  const require = createRequire(import.meta.url);
33
34
  const currentFilePath = fileURLToPath(import.meta.url);
@@ -37,19 +38,26 @@ const skillName = "boss-recommend-pipeline";
37
38
  const bundledSkillNames = [skillName, "boss-chat"];
38
39
  const exampleConfigPath = path.join(packageRoot, "config", "screening-config.example.json");
39
40
  const bossUrl = "https://www.zhipin.com/web/chat/recommend";
41
+ const bossLoginUrl = "https://www.zhipin.com/web/user/?ka=bticket";
40
42
  const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
43
+ const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
44
+ const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
41
45
  const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "openclaw"];
42
46
  const defaultMcpServerName = "boss-recommend";
43
47
  const defaultMcpCommand = "npx";
44
48
  const recommendMcpPackageName = "@reconcrap/boss-recommend-mcp";
45
49
  const recommendMcpBinaryName = "boss-recommend-mcp";
46
- const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h"]);
50
+ const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h", "list-jobs", "jobs", "recommend-jobs"]);
47
51
  const externalMcpTargetsEnv = "BOSS_RECOMMEND_MCP_CONFIG_TARGETS";
48
52
  const externalSkillDirsEnv = "BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS";
49
53
  const installConfigDefaults = Object.freeze({
50
54
  llmThinkingLevel: "low",
51
55
  humanRestEnabled: false
52
56
  });
57
+ const bossChatRuntimeChildDirs = ["logs", "runs", "profiles", "reports", "artifacts", "state"];
58
+ const bossChatCliUnsupportedStartCode = "CHAT_CLI_ASYNC_UNSUPPORTED_CDP_ONLY";
59
+ const calibrateUnsupportedCode = "CALIBRATE_UNSUPPORTED_CDP_ONLY";
60
+ const recommendCliRunUnsupportedCode = "RECOMMEND_CLI_RUN_UNSUPPORTED_CDP_ONLY";
53
61
 
54
62
  function getSkillSourceDir(name = skillName) {
55
63
  return path.join(packageRoot, "skills", name);
@@ -113,6 +121,125 @@ function pathExists(targetPath) {
113
121
  }
114
122
  }
115
123
 
124
+ function normalizeText(value) {
125
+ return String(value || "").replace(/\s+/g, " ").trim();
126
+ }
127
+
128
+ function isUnsafeRuntimeDirectory(targetPath) {
129
+ const resolved = path.resolve(String(targetPath || ""));
130
+ if (!resolved) return true;
131
+ if (path.parse(resolved).root.toLowerCase() === resolved.toLowerCase()) return true;
132
+ const normalized = resolved.replace(/\\/g, "/").toLowerCase();
133
+ if (process.platform === "win32") {
134
+ return (
135
+ normalized.endsWith("/windows")
136
+ || normalized.endsWith("/windows/system32")
137
+ || normalized.endsWith("/windows/syswow64")
138
+ || normalized.endsWith("/program files")
139
+ || normalized.endsWith("/program files (x86)")
140
+ );
141
+ }
142
+ return ["/system", "/usr", "/bin", "/sbin"].some((prefix) => (
143
+ normalized === prefix || normalized.startsWith(`${prefix}/`)
144
+ ));
145
+ }
146
+
147
+ function getBossChatRuntimeDirectories(runtime) {
148
+ return [
149
+ runtime.data_dir,
150
+ ...bossChatRuntimeChildDirs.map((name) => path.join(runtime.data_dir, name))
151
+ ];
152
+ }
153
+
154
+ function ensureBossChatRuntimeReadyLocal(workspaceRoot) {
155
+ const runtime = resolveCdpBossChatRuntimeLayout(workspaceRoot);
156
+ const runtimeDirectories = getBossChatRuntimeDirectories(runtime);
157
+ const created = [];
158
+ const existed = [];
159
+ const failed = [];
160
+ let migration = {
161
+ attempted: false,
162
+ performed: false,
163
+ source: runtime.migration_source_dir,
164
+ target: runtime.data_dir,
165
+ message: runtime.migration_source_dir
166
+ ? `Pending legacy boss-chat migration from ${runtime.migration_source_dir}`
167
+ : ""
168
+ };
169
+
170
+ if (isUnsafeRuntimeDirectory(runtime.data_dir)) {
171
+ return {
172
+ ...runtime,
173
+ directories: runtimeDirectories,
174
+ created,
175
+ existed,
176
+ failed: [
177
+ {
178
+ path: runtime.data_dir,
179
+ message: `Refusing unsafe boss-chat runtime path: ${runtime.data_dir}. Please use BOSS_CHAT_HOME in a writable user directory.`
180
+ }
181
+ ],
182
+ migration,
183
+ blocked_reason: "UNSAFE_DATA_DIR"
184
+ };
185
+ }
186
+
187
+ if (runtime.migration_source_dir) {
188
+ try {
189
+ fs.cpSync(runtime.migration_source_dir, runtime.data_dir, {
190
+ recursive: true,
191
+ force: false,
192
+ errorOnExist: false
193
+ });
194
+ migration = {
195
+ attempted: true,
196
+ performed: true,
197
+ source: runtime.migration_source_dir,
198
+ target: runtime.data_dir,
199
+ message: `Migrated legacy boss-chat runtime from ${runtime.migration_source_dir} to ${runtime.data_dir}. Legacy source was preserved.`
200
+ };
201
+ } catch (error) {
202
+ migration = {
203
+ attempted: true,
204
+ performed: false,
205
+ source: runtime.migration_source_dir,
206
+ target: runtime.data_dir,
207
+ message: error?.message || "Legacy boss-chat migration failed."
208
+ };
209
+ failed.push({
210
+ path: runtime.data_dir,
211
+ message: `Legacy migration failed: ${migration.message}`
212
+ });
213
+ }
214
+ }
215
+
216
+ for (const directory of runtimeDirectories) {
217
+ try {
218
+ const existedBefore = pathExists(directory);
219
+ ensureDir(directory);
220
+ if (existedBefore) {
221
+ existed.push(directory);
222
+ } else {
223
+ created.push(directory);
224
+ }
225
+ } catch (error) {
226
+ failed.push({
227
+ path: directory,
228
+ message: error?.message || String(error)
229
+ });
230
+ }
231
+ }
232
+
233
+ return {
234
+ ...runtime,
235
+ directories: runtimeDirectories,
236
+ created,
237
+ existed,
238
+ failed,
239
+ migration
240
+ };
241
+ }
242
+
116
243
  function readJsonObjectFileSafe(filePath) {
117
244
  if (!pathExists(filePath)) return {};
118
245
  try {
@@ -178,6 +305,235 @@ function getLegacyUserConfigPath() {
178
305
  return path.join(getCodexHome(), "boss-recommend-mcp", "screening-config.json");
179
306
  }
180
307
 
308
+ function getUserCalibrationPath() {
309
+ return path.join(getCodexHome(), "boss-recommend-mcp", "favorite-calibration.json");
310
+ }
311
+
312
+ function isUsableCalibrationFile(filePath) {
313
+ if (!filePath || !pathExists(filePath)) return false;
314
+ const parsed = readJsonObjectFileSafe(filePath);
315
+ return Boolean(
316
+ parsed
317
+ && parsed.favoritePosition
318
+ && Number.isFinite(parsed.favoritePosition.pageX)
319
+ && Number.isFinite(parsed.favoritePosition.pageY)
320
+ );
321
+ }
322
+
323
+ function resolveFavoriteCalibrationPath(workspaceRoot) {
324
+ const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_CALIBRATION_FILE || "");
325
+ if (fromEnv) return path.resolve(fromEnv);
326
+
327
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
328
+ const screenConfigPath = configResolution.config_path || getUserConfigPath();
329
+ const screenConfig = readJsonObjectFileSafe(screenConfigPath);
330
+ const calibrationFile = normalizeText(screenConfig?.calibrationFile || "");
331
+ if (calibrationFile && screenConfigPath) {
332
+ return path.resolve(path.dirname(screenConfigPath), calibrationFile);
333
+ }
334
+ return getUserCalibrationPath();
335
+ }
336
+
337
+ function resolveRecruitCalibrationScriptPath(workspaceRoot) {
338
+ const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_RECRUIT_CALIBRATION_SCRIPT || "");
339
+ const workspaceResolved = path.resolve(String(workspaceRoot || process.cwd()));
340
+ const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
341
+ const candidates = [
342
+ fromEnv,
343
+ path.join(workspaceResolved, "..", "..", "boss recruit pipeline", "boss-recruit-mcp", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
344
+ path.join(packageRoot, "..", "..", "boss recruit pipeline", "boss-recruit-mcp", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
345
+ path.join(appData, "npm", "node_modules", "@reconcrap", "boss-recruit-mcp", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
346
+ path.join(workspaceResolved, "..", "boss-recruit-mcp-main", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
347
+ path.join(packageRoot, "..", "boss-recruit-mcp-main", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs")
348
+ ].filter(Boolean).map((item) => path.resolve(item));
349
+
350
+ for (const candidate of new Set(candidates)) {
351
+ if (pathExists(candidate)) return candidate;
352
+ }
353
+ return null;
354
+ }
355
+
356
+ function getFeaturedCalibrationResolutionLocal(workspaceRoot) {
357
+ const calibrationPath = resolveFavoriteCalibrationPath(workspaceRoot);
358
+ return {
359
+ calibration_path: calibrationPath,
360
+ calibration_exists: pathExists(calibrationPath),
361
+ calibration_usable: isUsableCalibrationFile(calibrationPath),
362
+ calibration_script_path: resolveRecruitCalibrationScriptPath(workspaceRoot)
363
+ };
364
+ }
365
+
366
+ function runProcessSyncLocal({ command, args = [], cwd = process.cwd() } = {}) {
367
+ try {
368
+ const result = spawnSync(command, args, {
369
+ cwd,
370
+ encoding: "utf8",
371
+ env: process.env,
372
+ shell: false,
373
+ windowsHide: true
374
+ });
375
+ const stdout = String(result.stdout || "").trim();
376
+ const stderr = String(result.stderr || "").trim();
377
+ const output = [stdout, stderr].filter(Boolean).join("\n").trim();
378
+ return {
379
+ ok: result.status === 0 && !result.error,
380
+ status: Number.isInteger(result.status) ? result.status : -1,
381
+ stdout,
382
+ stderr,
383
+ output,
384
+ error_code: result.error?.code || null,
385
+ error_message: result.error?.message || ""
386
+ };
387
+ } catch (error) {
388
+ return {
389
+ ok: false,
390
+ status: -1,
391
+ stdout: "",
392
+ stderr: "",
393
+ output: "",
394
+ error_code: error.code || "SPAWN_FAILED",
395
+ error_message: error.message || String(error)
396
+ };
397
+ }
398
+ }
399
+
400
+ function parseMajorVersion(raw) {
401
+ const match = String(raw || "").match(/v?(\d+)(?:\.\d+){0,2}/);
402
+ if (!match) return null;
403
+ const major = Number.parseInt(match[1], 10);
404
+ return Number.isFinite(major) ? major : null;
405
+ }
406
+
407
+ function buildNodeCommandCheckLocal() {
408
+ const probe = runProcessSyncLocal({
409
+ command: "node",
410
+ args: ["--version"]
411
+ });
412
+ const major = parseMajorVersion(probe.output);
413
+ const versionOk = Number.isInteger(major) && major >= 18;
414
+ return {
415
+ key: "node_cli",
416
+ ok: probe.ok && versionOk,
417
+ path: "node --version",
418
+ message: probe.ok
419
+ ? (versionOk
420
+ ? `Node 命令可用 (${probe.output || "unknown version"})`
421
+ : `Node 版本过低 (${probe.output || "unknown version"}),要求 >= 18`)
422
+ : `未找到 node 命令,请先安装 Node.js >= 18。${probe.error_message ? ` (${probe.error_message})` : ""}`
423
+ };
424
+ }
425
+
426
+ function buildNodePackageCheckLocal({ key, moduleName, cwd, missingMessage }) {
427
+ if (!cwd || !pathExists(cwd)) {
428
+ return {
429
+ key,
430
+ ok: false,
431
+ path: moduleName,
432
+ module: moduleName,
433
+ install_cwd: null,
434
+ message: missingMessage
435
+ };
436
+ }
437
+ const probe = runProcessSyncLocal({
438
+ command: "node",
439
+ args: ["-e", `require.resolve(${JSON.stringify(moduleName)});`],
440
+ cwd
441
+ });
442
+ return {
443
+ key,
444
+ ok: probe.ok,
445
+ path: moduleName,
446
+ module: moduleName,
447
+ install_cwd: cwd,
448
+ message: probe.ok
449
+ ? `${moduleName} npm 依赖可用`
450
+ : `缺少 npm 依赖 ${moduleName},请在 boss-recommend-mcp 目录执行 npm install。`
451
+ };
452
+ }
453
+
454
+ function buildRuntimeDependencyChecksLocal({ dependencyDir = packageRoot } = {}) {
455
+ return [
456
+ buildNodeCommandCheckLocal(),
457
+ buildNodePackageCheckLocal({
458
+ key: "npm_dep_chrome_remote_interface",
459
+ moduleName: "chrome-remote-interface",
460
+ cwd: dependencyDir,
461
+ missingMessage: "无法校验 chrome-remote-interface:boss-recommend-mcp package 目录不存在。"
462
+ }),
463
+ buildNodePackageCheckLocal({
464
+ key: "npm_dep_ws",
465
+ moduleName: "ws",
466
+ cwd: dependencyDir,
467
+ missingMessage: "无法校验 ws:boss-recommend-mcp package 目录不存在。"
468
+ }),
469
+ buildNodePackageCheckLocal({
470
+ key: "npm_dep_sharp",
471
+ moduleName: "sharp",
472
+ cwd: dependencyDir,
473
+ missingMessage: "无法校验 sharp:boss-recommend-mcp package 目录不存在。"
474
+ })
475
+ ];
476
+ }
477
+
478
+ function resolveWorkspaceDebugPortLocal(workspaceRoot) {
479
+ const fromEnv = parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT);
480
+ if (fromEnv) return fromEnv;
481
+ const configResolution = getBossScreenConfigResolution(workspaceRoot);
482
+ const config = readJsonObjectFileSafe(configResolution.resolved_path);
483
+ return parsePositivePort(config?.debugPort) || 9222;
484
+ }
485
+
486
+ function buildScreenConfigCheckLocal(workspaceRoot, configResolution) {
487
+ const screenConfig = resolveBossScreeningConfig(workspaceRoot);
488
+ const pathForMessage = screenConfig.config_path || configResolution.resolved_path || configResolution.writable_path;
489
+ return {
490
+ key: "screen_config",
491
+ ok: screenConfig.ok,
492
+ path: pathForMessage,
493
+ reason: screenConfig.ok ? "OK" : (screenConfig.error?.code || "SCREEN_CONFIG_ERROR"),
494
+ message: screenConfig.ok ? "screening-config.json 可用" : (screenConfig.error?.message || "screening-config.json 不可用")
495
+ };
496
+ }
497
+
498
+ function runPipelinePreflightLocal(workspaceRoot, options = {}) {
499
+ const pageScope = normalizePageScope(options.pageScope) || "recommend";
500
+ const configResolution = getBossScreenConfigResolution(workspaceRoot);
501
+ const calibrationResolution = getFeaturedCalibrationResolutionLocal(workspaceRoot);
502
+ const checks = [
503
+ buildScreenConfigCheckLocal(workspaceRoot, configResolution),
504
+ {
505
+ key: "favorite_calibration",
506
+ ok: calibrationResolution.calibration_usable,
507
+ path: calibrationResolution.calibration_path,
508
+ optional: pageScope !== "featured",
509
+ message: calibrationResolution.calibration_usable
510
+ ? "favorite-calibration.json 可用"
511
+ : "favorite-calibration.json 不存在或无效(精选页收藏仅支持校准坐标点击)"
512
+ }
513
+ ];
514
+ checks.push(...buildRuntimeDependencyChecksLocal({ dependencyDir: packageRoot }));
515
+
516
+ const requiredCheckKeys = new Set([
517
+ "screen_config",
518
+ "node_cli",
519
+ "npm_dep_chrome_remote_interface",
520
+ "npm_dep_ws",
521
+ "npm_dep_sharp"
522
+ ]);
523
+ if (pageScope === "featured") {
524
+ requiredCheckKeys.add("favorite_calibration");
525
+ }
526
+
527
+ return {
528
+ ok: checks.every((item) => !requiredCheckKeys.has(item.key) || item.ok),
529
+ checks,
530
+ debug_port: resolveWorkspaceDebugPortLocal(workspaceRoot),
531
+ config_resolution: configResolution,
532
+ calibration_path: calibrationResolution.calibration_path,
533
+ page_scope: pageScope
534
+ };
535
+ }
536
+
181
537
  function getSkillTargetDir(name = skillName) {
182
538
  return path.join(getCodexHome(), "skills", name);
183
539
  }
@@ -224,6 +580,12 @@ function parsePositivePort(raw) {
224
580
  return Number.isFinite(port) && port > 0 ? port : null;
225
581
  }
226
582
 
583
+ function parseNonNegativeInteger(raw, fallback = undefined) {
584
+ if (raw === undefined || raw === null || raw === "") return fallback;
585
+ const parsed = Number.parseInt(String(raw), 10);
586
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
587
+ }
588
+
227
589
  function parseBossChatTargetCountOption(raw) {
228
590
  if (raw === undefined || raw === null) return undefined;
229
591
  const text = String(raw).trim();
@@ -670,9 +1032,9 @@ function pathStartsWith(filePath, rootPath) {
670
1032
  return file.startsWith(root);
671
1033
  }
672
1034
 
673
- function resolveCliConfigTarget(options = {}) {
1035
+ async function resolveCliConfigTarget(options = {}) {
674
1036
  const workspaceRoot = getWorkspaceRoot(options);
675
- const resolution = getScreenConfigResolution(workspaceRoot);
1037
+ const resolution = getBossScreenConfigResolution(workspaceRoot);
676
1038
  const workspacePreferred = (resolution.candidate_paths || []).find((item) => pathStartsWith(item, workspaceRoot)) || null;
677
1039
  const configPath = resolution.writable_path || resolution.resolved_path || workspacePreferred || getUserConfigPath();
678
1040
  return {
@@ -698,8 +1060,8 @@ function applyMissingInstallConfigDefaults(config = {}) {
698
1060
  };
699
1061
  }
700
1062
 
701
- function ensureUserConfig(options = {}) {
702
- const { configPath, workspacePreferred } = resolveCliConfigTarget(options);
1063
+ async function ensureUserConfig(options = {}) {
1064
+ const { configPath, workspacePreferred } = await resolveCliConfigTarget(options);
703
1065
  const writeTargets = dedupePaths([configPath, workspacePreferred]).filter(Boolean);
704
1066
  let lastError = null;
705
1067
  for (const targetPath of writeTargets) {
@@ -744,16 +1106,16 @@ function ensureUserConfig(options = {}) {
744
1106
  throw lastError || new Error("No writable target for screening-config.json");
745
1107
  }
746
1108
 
747
- function collectRuntimeDirectories(options = {}) {
1109
+ async function collectRuntimeDirectories(options = {}) {
748
1110
  const workspaceRoot = getWorkspaceRoot(options);
749
1111
  const stateHome = getStateHome();
750
- const runtime = resolveBossChatRuntimeLayout(workspaceRoot);
1112
+ const runtime = resolveCdpBossChatRuntimeLayout(workspaceRoot);
751
1113
  const bossChatRoot = runtime.data_dir;
752
1114
  const recommendRuntimeDirs = [
753
1115
  stateHome,
754
1116
  path.join(stateHome, "runs")
755
1117
  ];
756
- const bossChatRuntimeDirs = runtime.directories || [bossChatRoot];
1118
+ const bossChatRuntimeDirs = getBossChatRuntimeDirectories(runtime);
757
1119
  return {
758
1120
  workspaceRoot,
759
1121
  stateHome,
@@ -767,9 +1129,9 @@ function collectRuntimeDirectories(options = {}) {
767
1129
  };
768
1130
  }
769
1131
 
770
- function ensureRuntimeDirectories(options = {}) {
771
- const { workspaceRoot, stateHome } = collectRuntimeDirectories(options);
772
- const runtime = ensureBossChatRuntimeReady(workspaceRoot);
1132
+ async function ensureRuntimeDirectories(options = {}) {
1133
+ const { workspaceRoot, stateHome } = await collectRuntimeDirectories(options);
1134
+ const runtime = ensureBossChatRuntimeReadyLocal(workspaceRoot);
773
1135
  const recommendCreated = [];
774
1136
  const recommendExisted = [];
775
1137
  const failed = [...runtime.failed];
@@ -813,8 +1175,8 @@ function readJsonObjectFile(filePath) {
813
1175
  return parsed;
814
1176
  }
815
1177
 
816
- function loadBestExistingUserConfig(options = {}) {
817
- const { resolution, configPath, workspacePreferred } = resolveCliConfigTarget(options);
1178
+ async function loadBestExistingUserConfig(options = {}) {
1179
+ const { resolution, configPath, workspacePreferred } = await resolveCliConfigTarget(options);
818
1180
  const candidates = dedupePaths([
819
1181
  ...(resolution.candidate_paths || []),
820
1182
  configPath,
@@ -837,8 +1199,8 @@ function loadBestExistingUserConfig(options = {}) {
837
1199
  return { path: configPath, config: {} };
838
1200
  }
839
1201
 
840
- function writeConfigWithFallback(nextConfig, options = {}) {
841
- const { configPath, workspacePreferred } = resolveCliConfigTarget(options);
1202
+ async function writeConfigWithFallback(nextConfig, options = {}) {
1203
+ const { configPath, workspacePreferred } = await resolveCliConfigTarget(options);
842
1204
  const targets = dedupePaths([configPath, workspacePreferred]).filter(Boolean);
843
1205
  let lastError = null;
844
1206
  for (const target of targets) {
@@ -860,14 +1222,14 @@ function writeConfigWithFallback(nextConfig, options = {}) {
860
1222
  throw lastError || new Error("No writable target for screening-config.json");
861
1223
  }
862
1224
 
863
- function persistDebugPortSelection(port, options = {}) {
864
- const { config } = loadBestExistingUserConfig(options);
1225
+ async function persistDebugPortSelection(port, options = {}) {
1226
+ const { config } = await loadBestExistingUserConfig(options);
865
1227
  config.debugPort = port;
866
- const configPath = writeConfigWithFallback(config, options);
1228
+ const configPath = await writeConfigWithFallback(config, options);
867
1229
  return { port, configPath };
868
1230
  }
869
1231
 
870
- function setDebugPort(options = {}) {
1232
+ async function setDebugPort(options = {}) {
871
1233
  const selected = parsePositivePort(options.port);
872
1234
  if (!selected) {
873
1235
  throw new Error("Missing required --port <number> for set-port.");
@@ -876,7 +1238,7 @@ function setDebugPort(options = {}) {
876
1238
  return persistDebugPortSelection(selected, options);
877
1239
  }
878
1240
 
879
- function setScreeningConfig(options = {}) {
1241
+ async function setScreeningConfig(options = {}) {
880
1242
  const baseUrl = String(options["base-url"] || options.baseUrl || "").trim();
881
1243
  const apiKey = String(options["api-key"] || options.apiKey || "").trim();
882
1244
  const model = String(options.model || "").trim();
@@ -884,7 +1246,7 @@ function setScreeningConfig(options = {}) {
884
1246
  throw new Error("Missing required fields: --base-url, --api-key, --model");
885
1247
  }
886
1248
 
887
- const { config: existing } = loadBestExistingUserConfig(options);
1249
+ const { config: existing } = await loadBestExistingUserConfig(options);
888
1250
  const nextConfig = {
889
1251
  ...existing,
890
1252
  baseUrl,
@@ -909,7 +1271,7 @@ function setScreeningConfig(options = {}) {
909
1271
  if (debugPort) {
910
1272
  nextConfig.debugPort = debugPort;
911
1273
  }
912
- const configPath = writeConfigWithFallback(nextConfig, options);
1274
+ const configPath = await writeConfigWithFallback(nextConfig, options);
913
1275
  return { path: configPath, updated: true };
914
1276
  }
915
1277
 
@@ -926,6 +1288,20 @@ async function listChromeTabs(port) {
926
1288
  return Array.isArray(data) ? data : [];
927
1289
  }
928
1290
 
1291
+ function buildBossPageState(payload) {
1292
+ return {
1293
+ key: "boss_page_state",
1294
+ ...payload
1295
+ };
1296
+ }
1297
+
1298
+ function extractSampleUrls(tabs, limit = 5) {
1299
+ return tabs
1300
+ .map((tab) => tab?.url)
1301
+ .filter(Boolean)
1302
+ .slice(0, limit);
1303
+ }
1304
+
929
1305
  function findChromeOnboardingUrl(tabs) {
930
1306
  for (const tab of tabs) {
931
1307
  if (typeof tab?.url === "string" && chromeOnboardingUrlPattern.test(tab.url)) {
@@ -935,6 +1311,383 @@ function findChromeOnboardingUrl(tabs) {
935
1311
  return null;
936
1312
  }
937
1313
 
1314
+ function isBossRecommendTab(tab) {
1315
+ return typeof tab?.url === "string" && tab.url.includes("/web/chat/recommend");
1316
+ }
1317
+
1318
+ function findBossRecommendTab(tabs = []) {
1319
+ return tabs.find((tab) => isBossRecommendTab(tab)) || null;
1320
+ }
1321
+
1322
+ function isBossLoginTab(tab) {
1323
+ const url = String(tab?.url || "");
1324
+ const title = String(tab?.title || "");
1325
+ return (
1326
+ url === bossLoginUrl
1327
+ || bossLoginUrlPattern.test(url)
1328
+ || bossLoginTitlePattern.test(title)
1329
+ );
1330
+ }
1331
+
1332
+ function findBossPageTab(tabs = []) {
1333
+ return tabs.find((tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")) || null;
1334
+ }
1335
+
1336
+ function getNodeAttribute(node, name) {
1337
+ const attributes = node?.attributes || [];
1338
+ for (let index = 0; index < attributes.length; index += 2) {
1339
+ if (attributes[index] === name) return attributes[index + 1] || "";
1340
+ }
1341
+ return "";
1342
+ }
1343
+
1344
+ function uniqueMethodNames(methodLog = []) {
1345
+ return Array.from(new Set(methodLog.map((entry) => entry?.method).filter(Boolean)));
1346
+ }
1347
+
1348
+ async function inspectBossRecommendPageStateCdp(port, options = {}) {
1349
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 6000;
1350
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 1000;
1351
+ const expectedUrl = options.expectedUrl || bossUrl;
1352
+ const deadline = Date.now() + timeoutMs;
1353
+ let lastError = null;
1354
+ let lastTabs = [];
1355
+
1356
+ while (Date.now() <= deadline) {
1357
+ try {
1358
+ const tabs = await listChromeTabs(port);
1359
+ lastTabs = tabs;
1360
+ const recommendTab = findBossRecommendTab(tabs);
1361
+ if (recommendTab) {
1362
+ if (isBossLoginTab(recommendTab)) {
1363
+ return buildBossPageState({
1364
+ ok: false,
1365
+ state: "LOGIN_REQUIRED",
1366
+ path: recommendTab.url || bossLoginUrl,
1367
+ current_url: recommendTab.url || bossLoginUrl,
1368
+ title: recommendTab.title || null,
1369
+ requires_login: true,
1370
+ expected_url: expectedUrl,
1371
+ login_url: bossLoginUrl,
1372
+ message: "当前标签页虽在 recommend 路径,但检测到登录态页面特征,请先完成 Boss 登录。"
1373
+ });
1374
+ }
1375
+ return buildBossPageState({
1376
+ ok: true,
1377
+ state: "RECOMMEND_READY",
1378
+ path: recommendTab.url,
1379
+ current_url: recommendTab.url,
1380
+ title: recommendTab.title || null,
1381
+ requires_login: false,
1382
+ expected_url: expectedUrl,
1383
+ message: "Boss 推荐页已打开,且当前仍停留在 recommend 页面。"
1384
+ });
1385
+ }
1386
+
1387
+ const loginTab = tabs.find((tab) => isBossLoginTab(tab));
1388
+ if (loginTab) {
1389
+ return buildBossPageState({
1390
+ ok: false,
1391
+ state: "LOGIN_REQUIRED",
1392
+ path: loginTab.url || bossLoginUrl,
1393
+ current_url: loginTab.url || bossLoginUrl,
1394
+ title: loginTab.title || null,
1395
+ requires_login: true,
1396
+ expected_url: expectedUrl,
1397
+ login_url: bossLoginUrl,
1398
+ message: "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
1399
+ });
1400
+ }
1401
+
1402
+ const bossTab = findBossPageTab(tabs);
1403
+ if (bossTab) {
1404
+ const requiresLogin = bossLoginUrlPattern.test(bossTab.url);
1405
+ return buildBossPageState({
1406
+ ok: false,
1407
+ state: requiresLogin ? "LOGIN_REQUIRED" : "BOSS_NOT_ON_RECOMMEND",
1408
+ path: bossTab.url,
1409
+ current_url: bossTab.url,
1410
+ title: bossTab.title || null,
1411
+ requires_login: requiresLogin,
1412
+ expected_url: expectedUrl,
1413
+ login_url: requiresLogin ? bossLoginUrl : undefined,
1414
+ message: requiresLogin
1415
+ ? "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
1416
+ : "Boss 已登录但当前不在 recommend 页面,将尝试自动跳转。"
1417
+ });
1418
+ }
1419
+ } catch (error) {
1420
+ lastError = error;
1421
+ }
1422
+
1423
+ await sleepMs(pollMs);
1424
+ }
1425
+
1426
+ if (lastError) {
1427
+ return buildBossPageState({
1428
+ ok: false,
1429
+ state: "DEBUG_PORT_UNREACHABLE",
1430
+ path: `http://127.0.0.1:${port}`,
1431
+ current_url: null,
1432
+ title: null,
1433
+ requires_login: false,
1434
+ expected_url: expectedUrl,
1435
+ message: `无法连接到 Chrome DevTools 端口 ${port}。请确认 Chrome 已以远程调试模式启动。`,
1436
+ error: lastError.message
1437
+ });
1438
+ }
1439
+
1440
+ const onboardingUrl = findChromeOnboardingUrl(lastTabs);
1441
+ if (onboardingUrl) {
1442
+ return buildBossPageState({
1443
+ ok: false,
1444
+ state: "CHROME_ONBOARDING_INTERCEPTED",
1445
+ path: onboardingUrl,
1446
+ current_url: onboardingUrl,
1447
+ title: null,
1448
+ requires_login: false,
1449
+ expected_url: expectedUrl,
1450
+ message: "Chrome 当前停留在登录或引导页,尚未稳定到 Boss 推荐页。",
1451
+ sample_urls: extractSampleUrls(lastTabs)
1452
+ });
1453
+ }
1454
+
1455
+ return buildBossPageState({
1456
+ ok: false,
1457
+ state: "BOSS_TAB_NOT_FOUND",
1458
+ path: expectedUrl,
1459
+ current_url: null,
1460
+ title: null,
1461
+ requires_login: false,
1462
+ expected_url: expectedUrl,
1463
+ message: "未检测到 Boss 推荐页标签页。",
1464
+ sample_urls: extractSampleUrls(lastTabs)
1465
+ });
1466
+ }
1467
+
1468
+ async function withRecommendTargetCdp(port, callback) {
1469
+ const connection = await connectToChromeTarget({
1470
+ port,
1471
+ targetPredicate: (target) => isBossRecommendTab(target)
1472
+ });
1473
+ try {
1474
+ return await callback(connection);
1475
+ } finally {
1476
+ await connection.close();
1477
+ }
1478
+ }
1479
+
1480
+ async function bringBossRecommendTabToFrontCdp(port) {
1481
+ try {
1482
+ return await withRecommendTargetCdp(port, async ({ client, methodLog }) => {
1483
+ await enableDomains(client, ["Page"]);
1484
+ await bringPageToFront(client);
1485
+ assertNoForbiddenCdpCalls(methodLog);
1486
+ return {
1487
+ ok: true,
1488
+ method_log: uniqueMethodNames(methodLog)
1489
+ };
1490
+ });
1491
+ } catch (error) {
1492
+ return {
1493
+ ok: false,
1494
+ error: error.message || String(error)
1495
+ };
1496
+ }
1497
+ }
1498
+
1499
+ async function probeRecommendIframeStateCdp(port, options = {}) {
1500
+ const expectedUrl = options.expectedUrl || bossUrl;
1501
+ try {
1502
+ return await withRecommendTargetCdp(port, async ({ client, target, methodLog }) => {
1503
+ await enableDomains(client, ["Page", "DOM"]);
1504
+ const root = await getDocumentRoot(client, { depth: 1, pierce: true });
1505
+ const iframeNodeId = await querySelector(client, root.nodeId, 'iframe[name="recommendFrame"]');
1506
+ if (!iframeNodeId) {
1507
+ assertNoForbiddenCdpCalls(methodLog);
1508
+ return buildBossPageState({
1509
+ ok: false,
1510
+ state: "NO_RECOMMEND_IFRAME",
1511
+ path: target.url || expectedUrl,
1512
+ current_url: target.url || null,
1513
+ title: target.title || null,
1514
+ expected_url: expectedUrl,
1515
+ message: "recommend iframe 尚未挂载。",
1516
+ method_log: uniqueMethodNames(methodLog)
1517
+ });
1518
+ }
1519
+
1520
+ const described = await client.DOM.describeNode({
1521
+ nodeId: iframeNodeId,
1522
+ depth: 1,
1523
+ pierce: true
1524
+ });
1525
+ const iframeNode = described.node || {};
1526
+ const frameDocument = iframeNode.contentDocument || null;
1527
+ const frameUrl = frameDocument?.documentURL || getNodeAttribute(iframeNode, "src") || null;
1528
+ assertNoForbiddenCdpCalls(methodLog);
1529
+ if (!frameDocument?.nodeId) {
1530
+ return buildBossPageState({
1531
+ ok: false,
1532
+ state: "RECOMMEND_IFRAME_DOCUMENT_PENDING",
1533
+ path: target.url || expectedUrl,
1534
+ current_url: target.url || null,
1535
+ title: target.title || null,
1536
+ expected_url: expectedUrl,
1537
+ frame_url: frameUrl,
1538
+ message: "recommend iframe 已挂载但文档尚未就绪。",
1539
+ method_log: uniqueMethodNames(methodLog)
1540
+ });
1541
+ }
1542
+
1543
+ return buildBossPageState({
1544
+ ok: true,
1545
+ state: "RECOMMEND_IFRAME_READY",
1546
+ path: target.url || expectedUrl,
1547
+ current_url: target.url || null,
1548
+ title: target.title || null,
1549
+ expected_url: expectedUrl,
1550
+ frame_url: frameUrl,
1551
+ iframe_node_id: iframeNodeId,
1552
+ frame_document_node_id: frameDocument.nodeId,
1553
+ message: "recommend iframe 已通过 CDP DOM 检测就绪。",
1554
+ method_log: uniqueMethodNames(methodLog)
1555
+ });
1556
+ });
1557
+ } catch (error) {
1558
+ return buildBossPageState({
1559
+ ok: false,
1560
+ state: "RECOMMEND_IFRAME_PROBE_FAILED",
1561
+ path: expectedUrl,
1562
+ current_url: null,
1563
+ title: null,
1564
+ expected_url: expectedUrl,
1565
+ message: "recommend iframe CDP DOM 检测失败。",
1566
+ error: error.message || String(error)
1567
+ });
1568
+ }
1569
+ }
1570
+
1571
+ async function waitForRecommendIframeReadyCdp(port, options = {}) {
1572
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 6000;
1573
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 800;
1574
+ const deadline = Date.now() + timeoutMs;
1575
+ let lastState = null;
1576
+
1577
+ while (Date.now() <= deadline) {
1578
+ lastState = await probeRecommendIframeStateCdp(port, options);
1579
+ if (lastState?.state === "RECOMMEND_IFRAME_READY") return lastState;
1580
+ await sleepMs(pollMs);
1581
+ }
1582
+
1583
+ return lastState || buildBossPageState({
1584
+ ok: false,
1585
+ state: "NO_RECOMMEND_IFRAME",
1586
+ path: options.expectedUrl || bossUrl,
1587
+ current_url: null,
1588
+ title: null,
1589
+ expected_url: options.expectedUrl || bossUrl,
1590
+ message: "recommend iframe 尚未就绪。"
1591
+ });
1592
+ }
1593
+
1594
+ async function verifyRecommendPageStableCdp(port, options = {}) {
1595
+ const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1000;
1596
+ const recheckTimeoutMs = Number.isFinite(options.recheckTimeoutMs) ? options.recheckTimeoutMs : 6000;
1597
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 800;
1598
+
1599
+ await sleepMs(settleMs);
1600
+ const recheck = await inspectBossRecommendPageStateCdp(port, {
1601
+ timeoutMs: recheckTimeoutMs,
1602
+ pollMs
1603
+ });
1604
+ if (recheck.state !== "RECOMMEND_READY") return recheck;
1605
+
1606
+ const iframeState = await waitForRecommendIframeReadyCdp(port, {
1607
+ timeoutMs: recheckTimeoutMs,
1608
+ pollMs
1609
+ });
1610
+ if (iframeState.state === "RECOMMEND_IFRAME_READY") {
1611
+ return buildBossPageState({
1612
+ ...recheck,
1613
+ ok: true,
1614
+ state: "RECOMMEND_READY",
1615
+ frame_url: iframeState.frame_url || null,
1616
+ iframe_state: iframeState,
1617
+ method_log: iframeState.method_log || []
1618
+ });
1619
+ }
1620
+
1621
+ return buildBossPageState({
1622
+ ...iframeState,
1623
+ state: iframeState.state || "NO_RECOMMEND_IFRAME",
1624
+ message: iframeState.message || "Boss recommend 页面已打开,但 iframe 尚未就绪。"
1625
+ });
1626
+ }
1627
+
1628
+ async function navigateExistingTargetToBossRecommendCdp(port) {
1629
+ let connection = null;
1630
+ try {
1631
+ connection = await connectToChromeTarget({
1632
+ port,
1633
+ targetPredicate: (target) => target?.type === "page"
1634
+ });
1635
+ await enableDomains(connection.client, ["Page"]);
1636
+ await connection.client.Page.navigate({ url: bossUrl });
1637
+ assertNoForbiddenCdpCalls(connection.methodLog);
1638
+ return {
1639
+ ok: true,
1640
+ via: "cdp_page_navigate",
1641
+ target_id: connection.target?.id || null,
1642
+ method_log: uniqueMethodNames(connection.methodLog)
1643
+ };
1644
+ } catch (error) {
1645
+ return {
1646
+ ok: false,
1647
+ via: "cdp_page_navigate",
1648
+ error: error.message || String(error)
1649
+ };
1650
+ } finally {
1651
+ if (connection) await connection.close();
1652
+ }
1653
+ }
1654
+
1655
+ async function openBossRecommendTabCdp(port) {
1656
+ const endpoint = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(bossUrl)}`;
1657
+ const attempts = ["PUT", "GET"];
1658
+ let lastError = null;
1659
+
1660
+ for (const method of attempts) {
1661
+ try {
1662
+ const response = await fetch(endpoint, { method });
1663
+ if (response.ok) {
1664
+ let payload = null;
1665
+ try {
1666
+ payload = await response.json();
1667
+ } catch {}
1668
+ return {
1669
+ ok: true,
1670
+ via: "devtools_http_new_tab",
1671
+ method,
1672
+ target_id: payload?.id || null,
1673
+ current_url: payload?.url || bossUrl
1674
+ };
1675
+ }
1676
+ lastError = new Error(`DevTools /json/new returned ${response.status}`);
1677
+ } catch (error) {
1678
+ lastError = error;
1679
+ }
1680
+ }
1681
+
1682
+ const fallback = await navigateExistingTargetToBossRecommendCdp(port);
1683
+ if (fallback.ok) return fallback;
1684
+ return {
1685
+ ok: false,
1686
+ via: "devtools_http_new_tab",
1687
+ error: lastError?.message || fallback.error || "Failed to open Boss recommend tab via DevTools"
1688
+ };
1689
+ }
1690
+
938
1691
  function getDefaultChromeExecutableCandidates() {
939
1692
  const candidates = [process.env.BOSS_RECOMMEND_CHROME_PATH].filter(Boolean);
940
1693
  if (process.platform === "win32") {
@@ -997,16 +1750,118 @@ function resolveDefaultChromeUserDataDir(port) {
997
1750
  return legacyExisting || sharedPath;
998
1751
  }
999
1752
 
1753
+ function getLaunchChromeTiming(options = {}) {
1754
+ if (options["slow-live"] || options.slowLive) {
1755
+ return {
1756
+ initialTimeoutMs: 5000,
1757
+ inspectTimeoutMs: 20000,
1758
+ pollMs: 1000,
1759
+ settleMs: 2000
1760
+ };
1761
+ }
1762
+ return {
1763
+ initialTimeoutMs: 1500,
1764
+ inspectTimeoutMs: 6000,
1765
+ pollMs: 800,
1766
+ settleMs: 1000
1767
+ };
1768
+ }
1769
+
1770
+ async function ensureBossRecommendPageReadyCdp(port, options = {}) {
1771
+ const attempts = Number.isFinite(options.attempts) ? Math.max(0, options.attempts) : 3;
1772
+ const inspectTimeoutMs = Number.isFinite(options.inspectTimeoutMs) ? options.inspectTimeoutMs : 6000;
1773
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 800;
1774
+ const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1000;
1775
+
1776
+ let pageState = await inspectBossRecommendPageStateCdp(port, {
1777
+ timeoutMs: inspectTimeoutMs,
1778
+ pollMs
1779
+ });
1780
+ if (pageState.state === "RECOMMEND_READY") {
1781
+ const stableState = await verifyRecommendPageStableCdp(port, {
1782
+ settleMs,
1783
+ recheckTimeoutMs: inspectTimeoutMs,
1784
+ pollMs
1785
+ });
1786
+ return {
1787
+ ok: stableState.state === "RECOMMEND_READY",
1788
+ debug_port: port,
1789
+ state: stableState.state,
1790
+ page_state: stableState
1791
+ };
1792
+ }
1793
+
1794
+ if (pageState.state === "LOGIN_REQUIRED") {
1795
+ return {
1796
+ ok: false,
1797
+ debug_port: port,
1798
+ state: pageState.state,
1799
+ page_state: pageState
1800
+ };
1801
+ }
1802
+
1803
+ let openAttempt = null;
1804
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
1805
+ if (pageState.state === "DEBUG_PORT_UNREACHABLE" || pageState.state === "LOGIN_REQUIRED") break;
1806
+ openAttempt = await openBossRecommendTabCdp(port);
1807
+ await sleepMs(settleMs);
1808
+ pageState = await inspectBossRecommendPageStateCdp(port, {
1809
+ timeoutMs: inspectTimeoutMs,
1810
+ pollMs
1811
+ });
1812
+ if (pageState.state === "RECOMMEND_READY") {
1813
+ const stableState = await verifyRecommendPageStableCdp(port, {
1814
+ settleMs,
1815
+ recheckTimeoutMs: inspectTimeoutMs,
1816
+ pollMs
1817
+ });
1818
+ return {
1819
+ ok: stableState.state === "RECOMMEND_READY",
1820
+ debug_port: port,
1821
+ state: stableState.state,
1822
+ page_state: {
1823
+ ...stableState,
1824
+ open_attempt: openAttempt
1825
+ }
1826
+ };
1827
+ }
1828
+ if (pageState.state === "LOGIN_REQUIRED") break;
1829
+ }
1830
+
1831
+ return {
1832
+ ok: false,
1833
+ debug_port: port,
1834
+ state: pageState.state || "UNKNOWN",
1835
+ page_state: {
1836
+ ...pageState,
1837
+ open_attempt: openAttempt
1838
+ }
1839
+ };
1840
+ }
1841
+
1000
1842
  async function launchChrome(options = {}) {
1001
1843
  const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
1002
1844
  process.env.BOSS_RECOMMEND_CHROME_PORT = String(port);
1845
+ const timing = getLaunchChromeTiming(options);
1003
1846
 
1004
- const initialState = await inspectBossRecommendPageState(port, { timeoutMs: 1500, pollMs: 400 });
1847
+ const initialState = await inspectBossRecommendPageStateCdp(port, {
1848
+ timeoutMs: timing.initialTimeoutMs,
1849
+ pollMs: timing.pollMs
1850
+ });
1005
1851
  if (initialState.state !== "DEBUG_PORT_UNREACHABLE") {
1006
1852
  console.log(`Reusing existing Chrome debug instance on port ${port}`);
1007
- const pageState = await ensureBossRecommendPageReady(getWorkspaceRoot(options), { port, attempts: 2 });
1853
+ const pageState = await ensureBossRecommendPageReadyCdp(port, {
1854
+ attempts: 2,
1855
+ inspectTimeoutMs: timing.inspectTimeoutMs,
1856
+ pollMs: timing.pollMs,
1857
+ settleMs: timing.settleMs
1858
+ });
1008
1859
  if (pageState.ok) {
1009
1860
  console.log("Boss recommend page is ready.");
1861
+ const frontResult = await bringBossRecommendTabToFrontCdp(port);
1862
+ if (frontResult.ok) {
1863
+ console.log(`CDP methods: ${frontResult.method_log.join(", ") || "none"}`);
1864
+ }
1010
1865
  } else {
1011
1866
  console.log(pageState.page_state?.message || "Boss recommend page is not ready.");
1012
1867
  }
@@ -1037,9 +1892,19 @@ async function launchChrome(options = {}) {
1037
1892
  child.unref();
1038
1893
  console.log(`Chrome launched with remote debugging port ${port}`);
1039
1894
  console.log(`User data dir: ${userDataDir}`);
1040
- const pageState = await ensureBossRecommendPageReady(getWorkspaceRoot(options), { port, attempts: 6 });
1895
+ await sleepMs(timing.settleMs + 1200);
1896
+ const pageState = await ensureBossRecommendPageReadyCdp(port, {
1897
+ attempts: 6,
1898
+ inspectTimeoutMs: timing.inspectTimeoutMs,
1899
+ pollMs: timing.pollMs,
1900
+ settleMs: timing.settleMs
1901
+ });
1041
1902
  if (pageState.ok) {
1042
1903
  console.log("Boss recommend page is ready.");
1904
+ const frontResult = await bringBossRecommendTabToFrontCdp(port);
1905
+ if (frontResult.ok) {
1906
+ console.log(`CDP methods: ${frontResult.method_log.join(", ") || "none"}`);
1907
+ }
1043
1908
  } else {
1044
1909
  console.log(pageState.page_state?.message || "Boss recommend page is not ready.");
1045
1910
  }
@@ -1051,99 +1916,37 @@ function getCalibrationTimeoutMs(options = {}) {
1051
1916
  return Math.max(5000, parsed);
1052
1917
  }
1053
1918
 
1054
- async function calibrate(options = {}) {
1919
+ function buildUnsupportedCalibrateResponse(options = {}) {
1055
1920
  const workspaceRoot = getWorkspaceRoot(options);
1056
1921
  const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
1057
- process.env.BOSS_RECOMMEND_CHROME_PORT = String(port);
1058
- persistDebugPortSelection(port, options);
1059
1922
  const timeoutMs = getCalibrationTimeoutMs(options);
1060
1923
  const outputPath = String(options.output || "").trim()
1061
1924
  ? path.resolve(String(options.output))
1062
1925
  : null;
1063
-
1064
- console.log("Calibration checklist:");
1065
- console.log("0. 本校准仅用于推荐页“精选(tab)”的收藏点击定位。");
1066
- console.log("1. 工具会优先复用当前调试端口 Chrome,并确保 recommend 页面可访问。");
1067
- console.log("2. 工具会尝试自动切换到推荐页“精选”tab;校准过程中请不要再切换 tab。");
1068
- console.log("3. 先点一次收藏,再点一次取消收藏。");
1069
- console.log("4. 关闭详情页。");
1070
- console.log(`5. 校准监听窗口约 ${Math.round(timeoutMs / 1000)} 秒。`);
1071
- console.log("");
1072
-
1073
- const preState = await inspectBossRecommendPageState(port, { timeoutMs: 2000, pollMs: 500 });
1074
- if (preState.state === "DEBUG_PORT_UNREACHABLE") {
1075
- await launchChrome({ ...options, port: String(port) });
1076
- if (process.exitCode && process.exitCode !== 0) {
1077
- return;
1078
- }
1079
- } else {
1080
- console.log(`Detected existing Chrome debug instance on port ${port}; calibration will reuse it.`);
1081
- }
1082
-
1083
- const pageReady = await ensureBossRecommendPageReady(workspaceRoot, {
1084
- port,
1085
- attempts: 4
1086
- });
1087
- if (pageReady.ok) {
1088
- const switchResult = await switchRecommendTab(workspaceRoot, {
1089
- port,
1090
- target_status: "3"
1091
- });
1092
- if (switchResult?.ok) {
1093
- console.log("已自动切换到推荐页“精选”tab,请直接在当前页面打开人选详情并完成收藏/取消收藏。");
1094
- } else {
1095
- console.log("未能自动切换到“精选”tab,请手动切换到精选后再执行收藏/取消收藏。");
1096
- }
1097
- } else {
1098
- console.log("未能确认 recommend 页面就绪,请手动进入推荐页并切换到精选 tab 后再继续校准操作。");
1099
- }
1100
-
1101
- console.log(`等待你打开“精选”候选人详情页(最多 ${Math.round(timeoutMs / 1000)} 秒),检测到后自动开始校准监听...`);
1102
- const detailReady = await waitRecommendFeaturedDetailReady(workspaceRoot, {
1103
- port,
1104
- timeoutMs,
1105
- pollMs: 400
1106
- });
1107
- if (!detailReady.ok) {
1108
- console.error(detailReady.message || "未检测到可校准的精选详情页。");
1109
- console.error("请先打开任意精选候选人详情页并保持在前台,然后重新运行 calibrate。");
1110
- process.exitCode = 1;
1111
- return;
1112
- }
1113
- const detailSource = detailReady.detail_state?.source || "unknown";
1114
- const detailSelector = detailReady.detail_state?.selector || "unknown";
1115
- console.log(`已检测到详情页(source=${detailSource}, selector=${detailSelector}),即将启动校准脚本。`);
1116
- await new Promise((resolve) => setTimeout(resolve, 600));
1117
-
1118
- const result = await runRecommendCalibration(workspaceRoot, {
1926
+ return {
1927
+ status: "FAILED",
1928
+ error: {
1929
+ code: calibrateUnsupportedCode,
1930
+ message: "boss-recommend-mcp calibrate is fenced during the CDP-only rewrite because the old calibration route delegated to page-JS/Runtime-based adapter behavior and an external calibration script. A replacement must use CDP DOM/Input only and pass a live safe calibration gate before this command is re-enabled.",
1931
+ retryable: false
1932
+ },
1933
+ cdp_only: true,
1934
+ runtime_evaluate_used: false,
1935
+ method_summary: {},
1936
+ method_log: [],
1119
1937
  port,
1938
+ timeout_ms: timeoutMs,
1120
1939
  output: outputPath,
1121
- timeoutMs,
1122
- runtime: {
1123
- onOutput: (event) => {
1124
- const text = String(event?.text || "");
1125
- if (!text) return;
1126
- if (event?.stream === "stderr") {
1127
- process.stderr.write(text);
1128
- } else {
1129
- process.stdout.write(text);
1130
- }
1131
- }
1940
+ calibration_resolution: getFeaturedCalibrationResolutionLocal(workspaceRoot),
1941
+ guidance: {
1942
+ current_workaround: "Use an existing favorite-calibration.json if present; `doctor --page-scope featured` will report whether it is usable.",
1943
+ next_development_task: "Implement CDP-only featured detail/action discovery and a user-approved live calibration gate before restoring this command."
1132
1944
  }
1133
- });
1134
- if (result.ok) {
1135
- console.log(`Calibration saved: ${result.calibration_path}`);
1136
- return;
1137
- }
1945
+ };
1946
+ }
1138
1947
 
1139
- console.error(result.error?.message || "Calibration failed.");
1140
- console.error("如果你在校准开始后才从推荐切到精选,请先切到精选 tab 后重新运行 calibrate。");
1141
- if (result.calibration_script_path) {
1142
- console.error(`Calibration script: ${result.calibration_script_path}`);
1143
- }
1144
- if (result.calibration_path) {
1145
- console.error(`Calibration target: ${result.calibration_path}`);
1146
- }
1948
+ async function calibrate(options = {}) {
1949
+ printJson(buildUnsupportedCalibrateResponse(options));
1147
1950
  process.exitCode = 1;
1148
1951
  }
1149
1952
 
@@ -1215,11 +2018,22 @@ async function printDoctor(options = {}) {
1215
2018
  const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
1216
2019
  const workspaceRoot = getWorkspaceRoot(options);
1217
2020
  const pageScope = normalizePageScope(options["page-scope"] || options.pageScope) || "recommend";
1218
- const preflight = runPipelinePreflight(workspaceRoot, { pageScope });
2021
+ const preflight = runPipelinePreflightLocal(workspaceRoot, { pageScope });
1219
2022
  const checks = preflight.checks.slice();
1220
- const configResolution = getScreenConfigResolution(workspaceRoot);
1221
- const calibrationResolution = getFeaturedCalibrationResolution(workspaceRoot);
1222
- const pageState = await inspectBossRecommendPageState(port, { timeoutMs: 2000, pollMs: 500 });
2023
+ const configResolution = getBossScreenConfigResolution(workspaceRoot);
2024
+ const calibrationResolution = getFeaturedCalibrationResolutionLocal(workspaceRoot);
2025
+ const timing = getLaunchChromeTiming(options);
2026
+ let pageState = await inspectBossRecommendPageStateCdp(port, {
2027
+ timeoutMs: options["slow-live"] || options.slowLive ? timing.initialTimeoutMs : 2000,
2028
+ pollMs: options["slow-live"] || options.slowLive ? timing.pollMs : 500
2029
+ });
2030
+ if (pageState.state === "RECOMMEND_READY") {
2031
+ pageState = await verifyRecommendPageStableCdp(port, {
2032
+ settleMs: options["slow-live"] || options.slowLive ? timing.settleMs : 800,
2033
+ recheckTimeoutMs: options["slow-live"] || options.slowLive ? timing.inspectTimeoutMs : 3000,
2034
+ pollMs: options["slow-live"] || options.slowLive ? timing.pollMs : 500
2035
+ });
2036
+ }
1223
2037
  const resolvedConfigPath = configResolution.resolved_path || configResolution.writable_path;
1224
2038
  const userConfigExists = (
1225
2039
  (resolvedConfigPath && fs.existsSync(resolvedConfigPath))
@@ -1245,10 +2059,11 @@ async function printDoctor(options = {}) {
1245
2059
  checks.push({
1246
2060
  key: "featured_calibration_script",
1247
2061
  ok: Boolean(calibrationResolution.calibration_script_path),
2062
+ optional: true,
1248
2063
  path: calibrationResolution.calibration_script_path,
1249
2064
  message: calibrationResolution.calibration_script_path
1250
2065
  ? "已检测到 boss-recruit-mcp 校准脚本。"
1251
- : "未检测到 boss-recruit-mcp 校准脚本,精选页自动校准不可用。"
2066
+ : "未检测到 boss-recruit-mcp 校准脚本;CDP-only package 已禁用旧精选页自动校准。"
1252
2067
  });
1253
2068
  checks.push({
1254
2069
  key: "featured_calibration_file",
@@ -1322,7 +2137,7 @@ async function printDoctor(options = {}) {
1322
2137
  }
1323
2138
 
1324
2139
  printJson({
1325
- ok: checks.every((item) => item.ok),
2140
+ ok: checks.every((item) => item.ok || item.optional),
1326
2141
  port,
1327
2142
  checks,
1328
2143
  config_resolution: configResolution,
@@ -1340,8 +2155,8 @@ async function printDoctor(options = {}) {
1340
2155
  function printPaths() {
1341
2156
  const codexHome = getCodexHome();
1342
2157
  const stateHome = getStateHome();
1343
- const calibrationResolution = getFeaturedCalibrationResolution(process.cwd());
1344
- const bossChatRuntime = resolveBossChatRuntimeLayout(getWorkspaceRoot({}));
2158
+ const calibrationResolution = getFeaturedCalibrationResolutionLocal(process.cwd());
2159
+ const bossChatRuntime = resolveCdpBossChatRuntimeLayout(getWorkspaceRoot({}));
1345
2160
  console.log(`package_root=${packageRoot}`);
1346
2161
  console.log(`skill_sources=${bundledSkillNames.map((name) => getSkillSourceDir(name)).join(" | ")}`);
1347
2162
  console.log(`codex_home=${codexHome}`);
@@ -1362,8 +2177,9 @@ function printHelp() {
1362
2177
  console.log("Usage:");
1363
2178
  console.log(" boss-recommend-mcp Start the MCP server");
1364
2179
  console.log(" boss-recommend-mcp start Start the MCP server");
1365
- console.log(" boss-recommend-mcp run Run the recommend pipeline once via CLI and print JSON");
1366
- console.log(" boss-recommend-mcp chat <subcommand> Run bundled boss-chat commands via the recommend package");
2180
+ console.log(" boss-recommend-mcp run Disabled until the one-shot CLI has a CDP-only async replacement");
2181
+ console.log(" boss-recommend-mcp list-jobs CDP-only list of exact recommend job names for cron/one-shot inputs");
2182
+ console.log(" boss-recommend-mcp chat <subcommand> Run CDP-only boss-chat health/prepare/status commands");
1367
2183
  console.log(" boss-recommend-mcp install Install skill/MCP templates and auto-init screening-config.json (supports --agent trae-cn/cursor/...)");
1368
2184
  console.log(" boss-recommend-mcp install-skill Install bundled Codex skills");
1369
2185
  console.log(" boss-recommend-mcp init-config Create screening-config.json if missing (prefer workspace config/, fallback ~/.boss-recommend-mcp)");
@@ -1371,17 +2187,18 @@ function printHelp() {
1371
2187
  console.log(" boss-recommend-mcp set-port Persist preferred Chrome debug port to screening-config.json");
1372
2188
  console.log(" boss-recommend-mcp mcp-config Generate MCP config JSON for Cursor/Trae(含 trae-cn)/Claude Code/OpenClaw");
1373
2189
  console.log(" boss-recommend-mcp doctor Check config/runtime/calibration prerequisites (supports --agent trae-cn/cursor/...)");
1374
- console.log(" boss-recommend-mcp calibrate Run featured favorite calibration via recruit calibration script");
2190
+ console.log(" boss-recommend-mcp calibrate Disabled until CDP-only featured calibration is live-verified");
1375
2191
  console.log(" boss-recommend-mcp launch-chrome Launch or reuse Chrome debug instance and open Boss recommend page");
1376
2192
  console.log(" boss-recommend-mcp where Print installed package, skill, and config paths");
1377
2193
  console.log("");
1378
2194
  console.log("Run command:");
1379
- console.log(" boss-recommend-mcp run --instruction \"推荐页上筛选211男生,近14天没有,有大模型平台经验\" [--confirmation-json '{...}'] [--overrides-json '{...}'] [--follow-up-json '{...}']");
1380
- console.log(" boss-recommend-mcp chat run --job \"算法工程师\" --start-from unread --criteria \"有 AI Agent 经验\" --targetCount 20 [--greeting-text \"你好,方便发下简历吗?\"] # 后台启动,不自动轮询");
2195
+ console.log(" boss-recommend-mcp run --instruction \"推荐页上筛选211男生,近14天没有,有大模型平台经验\" # returns RECOMMEND_CLI_RUN_UNSUPPORTED_CDP_ONLY during rewrite; use MCP start_recommend_pipeline_run");
2196
+ console.log(" boss-recommend-mcp list-jobs --slow-live --port 9222");
2197
+ console.log(" boss-recommend-mcp chat prepare-run --slow-live --port 9222 # CDP-only preflight; start runs through MCP start_boss_chat_run");
1381
2198
  console.log(" boss-recommend-mcp config set --base-url <url> --api-key <key> --model <model> [--thinking-level off|low|medium|high|current] [--openai-organization <id>] [--openai-project <id>]");
1382
2199
  console.log(" boss-recommend-mcp install --agent trae-cn");
1383
2200
  console.log(" boss-recommend-mcp doctor --agent trae-cn --page-scope featured");
1384
- console.log(" boss-recommend-mcp calibrate --port 9222 [--timeout-ms 60000] [--output <path>]");
2201
+ console.log(" boss-recommend-mcp calibrate --port 9222 # returns CALIBRATE_UNSUPPORTED_CDP_ONLY during rewrite");
1385
2202
  }
1386
2203
 
1387
2204
  function printMcpConfig(options = {}) {
@@ -1397,10 +2214,10 @@ function printMcpConfig(options = {}) {
1397
2214
  }
1398
2215
  }
1399
2216
 
1400
- function installAll(options = {}) {
1401
- const runtimeDirsResult = ensureRuntimeDirectories(options);
2217
+ async function installAll(options = {}) {
2218
+ const runtimeDirsResult = await ensureRuntimeDirectories(options);
1402
2219
  const skillResults = installSkill();
1403
- const configResult = ensureUserConfig(options);
2220
+ const configResult = await ensureUserConfig(options);
1404
2221
  const mcpTemplateResult = writeMcpConfigFiles({ client: "all" });
1405
2222
  const externalMcpResult = installExternalMcpConfigs(options);
1406
2223
  const externalSkillResult = mirrorSkillToExternalDirs(options);
@@ -1458,26 +2275,78 @@ function installAll(options = {}) {
1458
2275
  }
1459
2276
  }
1460
2277
 
1461
- async function runPipelineOnce(options) {
2278
+ function buildUnsupportedRecommendCliRunResponse({
2279
+ instruction,
2280
+ confirmation,
2281
+ overrides,
2282
+ followUp,
2283
+ workspaceRoot,
2284
+ port
2285
+ } = {}) {
2286
+ return {
2287
+ status: "FAILED",
2288
+ error: {
2289
+ code: recommendCliRunUnsupportedCode,
2290
+ message: "boss-recommend-mcp run is fenced during the CDP-only rewrite because the old one-shot CLI route can reach page-JS/Runtime-based orchestration. Use the MCP tool start_recommend_pipeline_run for CDP-only recommend runs until a live-verified one-shot CLI replacement exists.",
2291
+ retryable: false
2292
+ },
2293
+ cdp_only: true,
2294
+ runtime_evaluate_used: false,
2295
+ method_summary: {},
2296
+ method_log: [],
2297
+ run_mode: "mcp_async_required",
2298
+ port,
2299
+ target_url: bossUrl,
2300
+ input: {
2301
+ workspace_root: workspaceRoot,
2302
+ instruction,
2303
+ confirmation: confirmation ?? null,
2304
+ overrides: overrides ?? null,
2305
+ follow_up: followUp ?? null
2306
+ },
2307
+ guidance: {
2308
+ recommended_tool: "start_recommend_pipeline_run",
2309
+ next_development_task: "Implement a CDP-only CLI wrapper that starts a durable shared run-service session, persists run state, and exits only after its live gate proves no Runtime.* methods are reachable."
2310
+ }
2311
+ };
2312
+ }
2313
+
2314
+ async function runPipelineOnce(options = {}) {
1462
2315
  const instruction = getRunInstruction(options);
1463
2316
  const confirmation = getRunConfirmation(options);
1464
2317
  const overrides = getRunOverrides(options);
1465
2318
  const followUp = getRunFollowUp(options);
1466
2319
  const workspaceRoot = getWorkspaceRoot(options);
1467
- const explicitPort = parsePositivePort(options.port);
1468
- if (explicitPort) {
1469
- process.env.BOSS_RECOMMEND_CHROME_PORT = String(explicitPort);
1470
- persistDebugPortSelection(explicitPort, options);
1471
- }
2320
+ const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
1472
2321
 
1473
- const result = await runRecommendPipeline({
2322
+ printJson(buildUnsupportedRecommendCliRunResponse({
1474
2323
  workspaceRoot,
1475
2324
  instruction,
1476
2325
  confirmation,
1477
2326
  overrides,
1478
- followUp
1479
- });
1480
- printJson(result);
2327
+ followUp,
2328
+ port
2329
+ }));
2330
+ process.exitCode = 1;
2331
+ }
2332
+
2333
+ function buildRecommendJobListCliInput(options = {}) {
2334
+ const targetUrlIncludes = String(options["target-url-includes"] || options.target_url_includes || "").trim();
2335
+ const host = String(options.host || "").trim();
2336
+ return {
2337
+ host: host || undefined,
2338
+ port: parsePositivePort(options.port),
2339
+ target_url_includes: targetUrlIncludes || undefined,
2340
+ allow_navigate: !(options["no-navigate"] === true || options.noNavigate === true || options.allow_navigate === false),
2341
+ slow_live: options["slow-live"] === true || options.slowLive === true || options.slow_live === true
2342
+ };
2343
+ }
2344
+
2345
+ async function listRecommendJobsCli(options = {}) {
2346
+ printJson(await listRecommendJobsTool({
2347
+ workspaceRoot: getWorkspaceRoot(options),
2348
+ args: buildRecommendJobListCliInput(options)
2349
+ }));
1481
2350
  }
1482
2351
 
1483
2352
  function buildBossChatCliInput(options = {}) {
@@ -1487,6 +2356,8 @@ function buildBossChatCliInput(options = {}) {
1487
2356
  ?? options.greetingText
1488
2357
  ?? options.greeting;
1489
2358
  const greetingText = typeof greetingTextRaw === "string" ? greetingTextRaw.trim() : undefined;
2359
+ const targetUrlIncludes = String(options["target-url-includes"] || options.target_url_includes || "").trim();
2360
+ const host = String(options.host || "").trim();
1490
2361
  return {
1491
2362
  profile: typeof options.profile === "string" ? options.profile.trim() : undefined,
1492
2363
  job: typeof options.job === "string" ? options.job.trim() : undefined,
@@ -1494,7 +2365,14 @@ function buildBossChatCliInput(options = {}) {
1494
2365
  criteria: typeof options.criteria === "string" ? options.criteria.trim() : undefined,
1495
2366
  greeting_text: greetingText || undefined,
1496
2367
  target_count: parseBossChatTargetCountOption(options.targetCount || options["target-count"] || options.target_count),
2368
+ host: host || undefined,
1497
2369
  port: parsePositivePort(options.port),
2370
+ target_url_includes: targetUrlIncludes || undefined,
2371
+ allow_navigate: !(options["no-navigate"] === true || options.noNavigate === true || options.allow_navigate === false),
2372
+ slow_live: options["slow-live"] === true || options.slowLive === true || options.slow_live === true,
2373
+ detail_limit: parseNonNegativeInteger(options["detail-limit"] ?? options.detail_limit),
2374
+ delay_ms: parseNonNegativeInteger(options["delay-ms"] ?? options.delay_ms),
2375
+ max_candidates: parseNonNegativeInteger(options["max-candidates"] ?? options.max_candidates),
1498
2376
  dry_run: options["dry-run"] === true || options.dryRun === true,
1499
2377
  no_state: options["no-state"] === true || options.noState === true,
1500
2378
  safe_pacing: parseBooleanOption(options["safe-pacing"] ?? options.safe_pacing),
@@ -1509,67 +2387,69 @@ function getBossChatCliRunTarget(options = {}) {
1509
2387
  };
1510
2388
  }
1511
2389
 
2390
+ function buildUnsupportedBossChatCliStartResponse(subcommand) {
2391
+ return {
2392
+ status: "FAILED",
2393
+ error: {
2394
+ code: bossChatCliUnsupportedStartCode,
2395
+ message: `boss-recommend-mcp chat ${subcommand} is fenced during the CDP-only rewrite because a one-shot CLI process cannot keep the live CDP session and run lifecycle alive after it exits. Use the MCP tool start_boss_chat_run, or the live chat harness, for CDP-only chat runs.`,
2396
+ retryable: false
2397
+ },
2398
+ cdp_only: true,
2399
+ runtime_evaluate_used: false,
2400
+ method_summary: {},
2401
+ method_log: []
2402
+ };
2403
+ }
2404
+
1512
2405
  async function runBossChatCliCommand(subcommand, options = {}) {
1513
2406
  const workspaceRoot = getWorkspaceRoot(options);
2407
+ const input = buildBossChatCliInput(options);
1514
2408
  if (subcommand === "health-check") {
1515
- printJson(getBossChatHealthCheck(workspaceRoot, {
1516
- port: parsePositivePort(options.port)
1517
- }));
1518
- return;
1519
- }
1520
-
1521
- if (subcommand === "prepare-run") {
1522
- printJson(await prepareBossChatRun({
2409
+ printJson(await bossChatHealthCheckTool({
1523
2410
  workspaceRoot,
1524
- input: buildBossChatCliInput(options)
2411
+ args: input
1525
2412
  }));
1526
2413
  return;
1527
2414
  }
1528
2415
 
1529
- if (subcommand === "run") {
1530
- printJson(await startBossChatRun({
2416
+ if (subcommand === "prepare-run") {
2417
+ printJson(await prepareBossChatRunTool({
1531
2418
  workspaceRoot,
1532
- input: buildBossChatCliInput(options)
2419
+ args: input
1533
2420
  }));
1534
2421
  return;
1535
2422
  }
1536
2423
 
1537
- if (subcommand === "start-run") {
1538
- printJson(await startBossChatRun({
1539
- workspaceRoot,
1540
- input: buildBossChatCliInput(options)
1541
- }));
2424
+ if (subcommand === "run" || subcommand === "start-run") {
2425
+ printJson(buildUnsupportedBossChatCliStartResponse(subcommand));
1542
2426
  return;
1543
2427
  }
1544
2428
 
1545
2429
  if (subcommand === "get-run") {
1546
- printJson(await getBossChatRun({
1547
- workspaceRoot,
1548
- input: getBossChatCliRunTarget(options)
2430
+ printJson(getBossChatRunTool({
2431
+ args: getBossChatCliRunTarget(options)
1549
2432
  }));
1550
2433
  return;
1551
2434
  }
1552
2435
 
1553
2436
  if (subcommand === "pause-run") {
1554
- printJson(await pauseBossChatRun({
1555
- workspaceRoot,
1556
- input: getBossChatCliRunTarget(options)
2437
+ printJson(pauseBossChatRunTool({
2438
+ args: getBossChatCliRunTarget(options)
1557
2439
  }));
1558
2440
  return;
1559
2441
  }
1560
2442
 
1561
2443
  if (subcommand === "resume-run") {
1562
- printJson(await resumeBossChatRun({
1563
- workspaceRoot,
1564
- input: getBossChatCliRunTarget(options)
2444
+ printJson(resumeBossChatRunTool({
2445
+ args: getBossChatCliRunTarget(options)
1565
2446
  }));
1566
2447
  return;
1567
2448
  }
1568
2449
 
1569
2450
  if (subcommand === "cancel-run") {
1570
- printJson(await cancelBossChatRun({
1571
- workspaceRoot,
1572
- input: getBossChatCliRunTarget(options)
2451
+ printJson(cancelBossChatRunTool({
2452
+ args: getBossChatCliRunTarget(options)
1573
2453
  }));
1574
2454
  return;
1575
2455
  }
@@ -1601,6 +2481,23 @@ export async function runCli(argv = process.argv) {
1601
2481
  process.exitCode = 1;
1602
2482
  }
1603
2483
  break;
2484
+ case "list-jobs":
2485
+ case "jobs":
2486
+ case "recommend-jobs":
2487
+ try {
2488
+ await listRecommendJobsCli(options);
2489
+ } catch (error) {
2490
+ printJson({
2491
+ status: "FAILED",
2492
+ error: {
2493
+ code: "RECOMMEND_JOB_LIST_CLI_FAILED",
2494
+ message: error.message || "Failed to list recommend jobs",
2495
+ retryable: true
2496
+ }
2497
+ });
2498
+ process.exitCode = 1;
2499
+ }
2500
+ break;
1604
2501
  case "chat":
1605
2502
  try {
1606
2503
  const chatSubcommand = String(argv[3] || "").trim().toLowerCase();
@@ -1620,7 +2517,7 @@ export async function runCli(argv = process.argv) {
1620
2517
  break;
1621
2518
  case "install":
1622
2519
  try {
1623
- installAll(options);
2520
+ await installAll(options);
1624
2521
  } catch (error) {
1625
2522
  console.error(error.message || "Install failed.");
1626
2523
  process.exitCode = 1;
@@ -1632,8 +2529,8 @@ export async function runCli(argv = process.argv) {
1632
2529
  }
1633
2530
  break;
1634
2531
  case "init-config": {
1635
- const runtimeDirsResult = ensureRuntimeDirectories(options);
1636
- const result = ensureUserConfig(options);
2532
+ const runtimeDirsResult = await ensureRuntimeDirectories(options);
2533
+ const result = await ensureUserConfig(options);
1637
2534
  console.log(
1638
2535
  `Runtime directories prepared: created=${runtimeDirsResult.created.length}, existing=${runtimeDirsResult.existed.length}, failed=${runtimeDirsResult.failed.length}`
1639
2536
  );
@@ -1657,7 +2554,7 @@ export async function runCli(argv = process.argv) {
1657
2554
  }
1658
2555
  case "set-port": {
1659
2556
  try {
1660
- const result = setDebugPort(options);
2557
+ const result = await setDebugPort(options);
1661
2558
  console.log(`Preferred debug port saved: ${result.port}`);
1662
2559
  console.log(`Updated config: ${result.configPath}`);
1663
2560
  console.log("Port priority for runtime commands: --port > BOSS_RECOMMEND_CHROME_PORT > screening-config.json.debugPort > 9222");
@@ -1668,8 +2565,8 @@ export async function runCli(argv = process.argv) {
1668
2565
  break;
1669
2566
  }
1670
2567
  case "set-config": {
1671
- try {
1672
- const result = setScreeningConfig(options);
2568
+ try {
2569
+ const result = await setScreeningConfig(options);
1673
2570
  console.log(`screening-config.json updated: ${result.path}`);
1674
2571
  } catch (error) {
1675
2572
  console.error(error.message || "Failed to write screening-config.json.");
@@ -1682,7 +2579,7 @@ export async function runCli(argv = process.argv) {
1682
2579
  if (!sub || sub.startsWith("--") || sub === "set") {
1683
2580
  const configOptions = sub === "set" ? parseOptions(argv.slice(4)) : options;
1684
2581
  try {
1685
- const result = setScreeningConfig(configOptions);
2582
+ const result = await setScreeningConfig(configOptions);
1686
2583
  console.log(`screening-config.json updated: ${result.path}`);
1687
2584
  } catch (error) {
1688
2585
  console.error(error.message || "Failed to write screening-config.json.");
@@ -1712,7 +2609,7 @@ export async function runCli(argv = process.argv) {
1712
2609
  await launchChrome(options);
1713
2610
  break;
1714
2611
  case "where":
1715
- printPaths();
2612
+ await printPaths();
1716
2613
  break;
1717
2614
  case "help":
1718
2615
  case "--help":
@@ -1727,18 +2624,20 @@ export async function runCli(argv = process.argv) {
1727
2624
  }
1728
2625
 
1729
2626
  export const __testables = {
2627
+ buildRecommendJobListCliInput,
1730
2628
  buildBossChatCliInput,
1731
2629
  buildDefaultMcpArgs,
1732
2630
  buildMcpLaunchConfig,
2631
+ buildUnsupportedRecommendCliRunResponse,
1733
2632
  collectRuntimeDirectories,
1734
- ensureBossChatRuntimeReady,
2633
+ ensureBossChatRuntimeReady: ensureBossChatRuntimeReadyLocal,
1735
2634
  ensureRuntimeDirectories,
1736
2635
  getBossChatCliRunTarget,
1737
2636
  getDefaultMcpPackageSpecifier,
1738
2637
  getRunFollowUp,
1739
2638
  installSkill,
1740
2639
  isInstalledPackageRoot,
1741
- resolveBossChatRuntimeLayout,
2640
+ resolveBossChatRuntimeLayout: resolveCdpBossChatRuntimeLayout,
1742
2641
  runBossChatCliCommand,
1743
2642
  runPipelineOnce
1744
2643
  };