@reconcrap/boss-recommend-mcp 2.1.1 → 2.1.3

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/src/cli.js CHANGED
@@ -28,6 +28,10 @@ import {
28
28
  prepareRecommendPipelineRunTool,
29
29
  startRecommendPipelineRunTool
30
30
  } from "./recommend-mcp.js";
31
+ import {
32
+ getRecommendScheduledRunTool,
33
+ scheduleRecommendPipelineRunTool
34
+ } from "./recommend-scheduler.js";
31
35
  import {
32
36
  getBossScreenConfigResolution,
33
37
  resolveBossChatRuntimeLayout as resolveCdpBossChatRuntimeLayout,
@@ -50,10 +54,12 @@ const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|his
50
54
  const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
51
55
  const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
52
56
  const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "openclaw", "qclaw"];
53
- const defaultMcpServerName = "boss-recommend";
54
- const defaultMcpCommand = "npx";
55
- const recommendMcpPackageName = "@reconcrap/boss-recommend-mcp";
56
- const recommendMcpBinaryName = "boss-recommend-mcp";
57
+ const defaultMcpServerName = "boss-recommend";
58
+ const defaultMcpCommand = "npx";
59
+ const recommendMcpPackageName = "@reconcrap/boss-recommend-mcp";
60
+ const recommendMcpBinaryName = "boss-recommend-mcp";
61
+ const globalMcpWrapperFileName = "boss-recommend-mcp-mcp-server";
62
+ const supportedMcpLaunchModes = ["npx", "global-wrapper"];
57
63
  const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h", "list-jobs", "jobs", "recommend-jobs"]);
58
64
  const externalMcpTargetsEnv = "BOSS_RECOMMEND_MCP_CONFIG_TARGETS";
59
65
  const externalSkillDirsEnv = "BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS";
@@ -124,15 +130,22 @@ function getCodexHome() {
124
130
  : path.join(os.homedir(), ".codex");
125
131
  }
126
132
 
127
- function getStateHome() {
128
- return process.env.BOSS_RECOMMEND_HOME
129
- ? path.resolve(process.env.BOSS_RECOMMEND_HOME)
130
- : path.join(os.homedir(), ".boss-recommend-mcp");
131
- }
132
-
133
- function ensureDir(targetPath) {
134
- fs.mkdirSync(targetPath, { recursive: true });
135
- }
133
+ function getStateHome() {
134
+ return process.env.BOSS_RECOMMEND_HOME
135
+ ? path.resolve(process.env.BOSS_RECOMMEND_HOME)
136
+ : path.join(os.homedir(), ".boss-recommend-mcp");
137
+ }
138
+
139
+ function getGlobalMcpWrapperPath(options = {}) {
140
+ if (typeof options["mcp-wrapper-path"] === "string" && options["mcp-wrapper-path"].trim()) {
141
+ return path.resolve(options["mcp-wrapper-path"].trim());
142
+ }
143
+ return path.join(getStateHome(), "bin", globalMcpWrapperFileName);
144
+ }
145
+
146
+ function ensureDir(targetPath) {
147
+ fs.mkdirSync(targetPath, { recursive: true });
148
+ }
136
149
 
137
150
  function pathExists(targetPath) {
138
151
  try {
@@ -738,13 +751,55 @@ function parseMcpClientTargets(rawValue) {
738
751
  return unique;
739
752
  }
740
753
 
741
- function isPlainObject(value) {
742
- return value && typeof value === "object" && !Array.isArray(value);
743
- }
744
-
745
- function shouldDefaultRecommendDetachedMcpEnv(options = {}) {
746
- const client = normalizeMcpClientName(options.client);
747
- const agent = normalizeAgentName(options.agent);
754
+ function isPlainObject(value) {
755
+ return value && typeof value === "object" && !Array.isArray(value);
756
+ }
757
+
758
+ function normalizeMcpLaunchMode(options = {}) {
759
+ const raw = String(options["mcp-launch"] || options.mcpLaunch || "").trim().toLowerCase().replace(/_/g, "-");
760
+ if (!raw) return "npx";
761
+ if (raw === "default") return "npx";
762
+ if (raw === "global" || raw === "wrapper") return "global-wrapper";
763
+ if (supportedMcpLaunchModes.includes(raw)) return raw;
764
+ throw new Error(`Unsupported --mcp-launch value: ${raw}. Supported: ${supportedMcpLaunchModes.join(", ")}`);
765
+ }
766
+
767
+ function buildGlobalMcpWrapperScript() {
768
+ return `#!/usr/bin/env bash
769
+ set -euo pipefail
770
+
771
+ export NVM_DIR="\${NVM_DIR:-$HOME/.nvm}"
772
+ if [ -s "$NVM_DIR/nvm.sh" ]; then
773
+ . "$NVM_DIR/nvm.sh"
774
+ elif [ -s "$HOME/.nvm/nvm.sh" ]; then
775
+ export NVM_DIR="$HOME/.nvm"
776
+ . "$NVM_DIR/nvm.sh"
777
+ fi
778
+
779
+ if ! command -v ${recommendMcpBinaryName} >/dev/null 2>&1; then
780
+ echo "${recommendMcpBinaryName} not found on PATH. Install or reload nvm, then run: npm -g i ${recommendMcpPackageName}@latest" >&2
781
+ exit 127
782
+ fi
783
+
784
+ exec ${recommendMcpBinaryName} start "$@"
785
+ `;
786
+ }
787
+
788
+ function ensureGlobalMcpWrapper(options = {}) {
789
+ const wrapperPath = getGlobalMcpWrapperPath(options);
790
+ ensureDir(path.dirname(wrapperPath));
791
+ fs.writeFileSync(wrapperPath, buildGlobalMcpWrapperScript(), "utf8");
792
+ try {
793
+ fs.chmodSync(wrapperPath, 0o755);
794
+ } catch {
795
+ // Some filesystems ignore POSIX executable bits; the path is still valid for POSIX hosts.
796
+ }
797
+ return wrapperPath;
798
+ }
799
+
800
+ function shouldDefaultRecommendDetachedMcpEnv(options = {}) {
801
+ const client = normalizeMcpClientName(options.client);
802
+ const agent = normalizeAgentName(options.agent);
748
803
  return client === "openclaw"
749
804
  || client === "qclaw"
750
805
  || agent === "openclaw"
@@ -764,10 +819,28 @@ function getAgentConfigOutputDir(options = {}) {
764
819
  return path.join(getStateHome(), "agent-mcp-configs");
765
820
  }
766
821
 
767
- function buildMcpLaunchConfig(options = {}) {
768
- const command = typeof options.command === "string" && options.command.trim()
769
- ? options.command.trim()
770
- : defaultMcpCommand;
822
+ function buildMcpLaunchConfig(options = {}) {
823
+ const mcpLaunchMode = normalizeMcpLaunchMode(options);
824
+ if (mcpLaunchMode === "global-wrapper") {
825
+ const args = parseJsonOption(options["args-json"], "args-json");
826
+ const env = parseJsonOption(options["env-json"], "env-json");
827
+ const launchConfig = {
828
+ command: ensureGlobalMcpWrapper(options),
829
+ args: Array.isArray(args) ? args : []
830
+ };
831
+ const mergedEnv = {
832
+ ...getDefaultMcpEnv(options),
833
+ ...(isPlainObject(env) ? env : {})
834
+ };
835
+ if (Object.keys(mergedEnv).length > 0) {
836
+ launchConfig.env = mergedEnv;
837
+ }
838
+ return launchConfig;
839
+ }
840
+
841
+ const command = typeof options.command === "string" && options.command.trim()
842
+ ? options.command.trim()
843
+ : defaultMcpCommand;
771
844
  const args = parseJsonOption(options["args-json"], "args-json");
772
845
  const env = parseJsonOption(options["env-json"], "env-json");
773
846
  const launchArgs = Array.isArray(args) && args.length > 0
@@ -2625,6 +2698,8 @@ function printHelp() {
2625
2698
  console.log(" boss-recommend-mcp Start the MCP server");
2626
2699
  console.log(" boss-recommend-mcp start Start the MCP server");
2627
2700
  console.log(" boss-recommend-mcp prepare-run Validate a cron-ready recommend run payload without starting screening");
2701
+ console.log(" boss-recommend-mcp schedule-run Create a package-owned delayed recommend run");
2702
+ console.log(" boss-recommend-mcp schedule-status Check a package-owned delayed recommend run");
2628
2703
  console.log(" boss-recommend-mcp run Start a CDP-only recommend run through the shared run service");
2629
2704
  console.log(" boss-recommend-mcp list-jobs CDP-only list of exact recommend job names for cron/one-shot inputs");
2630
2705
  console.log(" boss-recommend-mcp chat <subcommand> Run CDP-only boss-chat health/prepare/status commands");
@@ -2633,14 +2708,17 @@ function printHelp() {
2633
2708
  console.log(" boss-recommend-mcp init-config Create screening-config.json if missing (prefer workspace config/, fallback ~/.boss-recommend-mcp)");
2634
2709
  console.log(" boss-recommend-mcp config set Write baseUrl/apiKey/model (prefer workspace config/, fallback ~/.boss-recommend-mcp)");
2635
2710
  console.log(" boss-recommend-mcp set-port Persist preferred Chrome debug port to screening-config.json");
2636
- console.log(" boss-recommend-mcp mcp-config Generate MCP config JSON for Cursor/Trae(含 trae-cn)/Claude Code/OpenClaw/QClaw");
2637
- console.log(" boss-recommend-mcp doctor Check config/runtime/calibration prerequisites (supports --agent trae-cn/qclaw/cursor/...)");
2638
- console.log(" boss-recommend-mcp calibrate Disabled until CDP-only featured calibration is live-verified");
2639
- console.log(" boss-recommend-mcp launch-chrome Launch or reuse Chrome debug instance and open Boss recommend page");
2640
- console.log(" boss-recommend-mcp where Print installed package, skill, and config paths");
2711
+ console.log(" boss-recommend-mcp mcp-config Generate MCP config JSON for Cursor/Trae(含 trae-cn)/Claude Code/OpenClaw/QClaw");
2712
+ console.log(" boss-recommend-mcp doctor Check config/runtime/calibration prerequisites (supports --agent trae-cn/qclaw/cursor/...)");
2713
+ console.log(" boss-recommend-mcp calibrate Disabled until CDP-only featured calibration is live-verified");
2714
+ console.log(" boss-recommend-mcp launch-chrome Launch or reuse Chrome debug instance and open Boss recommend page");
2715
+ console.log(" boss-recommend-mcp where Print installed package, skill, and config paths");
2716
+ console.log(" boss-recommend-mcp install --mcp-launch global-wrapper Use ~/.boss-recommend-mcp/bin wrapper so npm global upgrades affect MCP hosts");
2641
2717
  console.log("");
2642
2718
  console.log("Run command:");
2643
2719
  console.log(" boss-recommend-mcp prepare-run --instruction \"...\" --overrides-file overrides.json --confirmation-file confirmation.json");
2720
+ console.log(" boss-recommend-mcp schedule-run --schedule-delay-minutes 10 --instruction-file boss-recommend-instruction.txt --overrides-file overrides.json --confirmation-file confirmation.json");
2721
+ console.log(" boss-recommend-mcp schedule-status --schedule-id <id>");
2644
2722
  console.log(" boss-recommend-mcp run --instruction \"推荐页上筛选211男生,近14天没有,有大模型平台经验\" --overrides-file overrides.json --confirmation-file confirmation.json");
2645
2723
  console.log(" boss-recommend-mcp run --detached --instruction \"...\" --overrides-file overrides.json --confirmation-file confirmation.json");
2646
2724
  console.log(" boss-recommend-mcp list-jobs --slow-live --port 9222");
@@ -2665,14 +2743,15 @@ function printMcpConfig(options = {}) {
2665
2743
  }
2666
2744
  }
2667
2745
 
2668
- async function installAll(options = {}) {
2669
- const runtimeDirsResult = await ensureRuntimeDirectories(options);
2670
- const skillResults = installSkill();
2671
- const configResult = await ensureUserConfig(options);
2672
- const mcpTemplateResult = writeMcpConfigFiles({ client: "all" });
2673
- const externalMcpResult = installExternalMcpConfigs(options);
2674
- const externalSkillResult = mirrorSkillToExternalDirs(options);
2675
- console.log(
2746
+ async function installAll(options = {}) {
2747
+ const runtimeDirsResult = await ensureRuntimeDirectories(options);
2748
+ const skillResults = installSkill();
2749
+ const configResult = await ensureUserConfig(options);
2750
+ const mcpLaunchMode = normalizeMcpLaunchMode(options);
2751
+ const mcpTemplateResult = writeMcpConfigFiles({ ...options, agent: undefined, client: "all" });
2752
+ const externalMcpResult = installExternalMcpConfigs(options);
2753
+ const externalSkillResult = mirrorSkillToExternalDirs(options);
2754
+ console.log(
2676
2755
  `Runtime directories prepared: created=${runtimeDirsResult.created.length}, existing=${runtimeDirsResult.existed.length}, failed=${runtimeDirsResult.failed.length}`
2677
2756
  );
2678
2757
  console.log(`- recommend runtime: ${runtimeDirsResult.stateHome}`);
@@ -2700,10 +2779,13 @@ async function installAll(options = {}) {
2700
2779
  console.warn(`screening-config.json skip default patch: ${configResult.patch_error}`);
2701
2780
  }
2702
2781
  console.log(`请在该目录修改 baseUrl/apiKey/model 并替换占位词后再运行:${path.dirname(configResult.path)}`);
2703
- console.log(`MCP config templates exported to: ${mcpTemplateResult.outputDir}`);
2704
- for (const item of mcpTemplateResult.files) {
2705
- console.log(`- ${item.client}: ${item.file}`);
2706
- }
2782
+ console.log(`MCP config templates exported to: ${mcpTemplateResult.outputDir}`);
2783
+ for (const item of mcpTemplateResult.files) {
2784
+ console.log(`- ${item.client}: ${item.file}`);
2785
+ }
2786
+ if (mcpLaunchMode === "global-wrapper") {
2787
+ console.log(`Upgrade-stable MCP wrapper: ${getGlobalMcpWrapperPath(options)}`);
2788
+ }
2707
2789
  if (externalMcpResult.targets.length > 0) {
2708
2790
  console.log(`Auto-configured external MCP files: ${externalMcpResult.applied.length}`);
2709
2791
  for (const item of externalMcpResult.applied) {
@@ -2833,6 +2915,62 @@ async function preparePipelineOnce(options = {}) {
2833
2915
  }
2834
2916
  }
2835
2917
 
2918
+ function addScheduleOptions(args, options = {}) {
2919
+ const scheduleId = String(options["schedule-id"] || options.schedule_id || options.scheduleId || "").trim();
2920
+ if (scheduleId) args.schedule_id = scheduleId;
2921
+ const runAt = String(options["schedule-run-at"] || options.schedule_run_at || options.scheduleRunAt || options["run-at"] || options.run_at || "").trim();
2922
+ if (runAt) args.schedule_run_at = runAt;
2923
+ const delayMinutes = parseNonNegativeInteger(options["schedule-delay-minutes"] ?? options.schedule_delay_minutes ?? options.scheduleDelayMinutes);
2924
+ if (delayMinutes !== undefined) args.schedule_delay_minutes = delayMinutes;
2925
+ const delaySeconds = parseNonNegativeInteger(options["schedule-delay-seconds"] ?? options.schedule_delay_seconds ?? options.scheduleDelaySeconds);
2926
+ if (delaySeconds !== undefined) args.schedule_delay_seconds = delaySeconds;
2927
+ return args;
2928
+ }
2929
+
2930
+ async function schedulePipelineOnce(options = {}) {
2931
+ const workspaceRoot = getWorkspaceRoot(options);
2932
+ const { args, port } = buildRecommendRunCliInput(options);
2933
+ addScheduleOptions(args, options);
2934
+ const result = await scheduleRecommendPipelineRunTool({
2935
+ workspaceRoot,
2936
+ args
2937
+ });
2938
+ printJson({
2939
+ ...result,
2940
+ cli: {
2941
+ ...(result.cli || {}),
2942
+ command: "schedule-run",
2943
+ cdp_only: true,
2944
+ package_owned_scheduler: true,
2945
+ workspace_root: workspaceRoot,
2946
+ port
2947
+ }
2948
+ });
2949
+ if (result.status !== "SCHEDULED") {
2950
+ process.exitCode = 1;
2951
+ }
2952
+ }
2953
+
2954
+ function scheduleStatusCli(options = {}) {
2955
+ const scheduleId = String(options["schedule-id"] || options.schedule_id || options.scheduleId || "").trim();
2956
+ const result = getRecommendScheduledRunTool({
2957
+ args: {
2958
+ schedule_id: scheduleId
2959
+ }
2960
+ });
2961
+ printJson({
2962
+ ...result,
2963
+ cli: {
2964
+ command: "schedule-status",
2965
+ cdp_only: true,
2966
+ package_owned_scheduler: true
2967
+ }
2968
+ });
2969
+ if (result.status !== "OK") {
2970
+ process.exitCode = 1;
2971
+ }
2972
+ }
2973
+
2836
2974
  async function runPipelineOnce(options = {}) {
2837
2975
  const workspaceRoot = getWorkspaceRoot(options);
2838
2976
  const { args, port } = buildRecommendRunCliInput(options);
@@ -3046,6 +3184,39 @@ export async function runCli(argv = process.argv) {
3046
3184
  process.exitCode = 1;
3047
3185
  }
3048
3186
  break;
3187
+ case "schedule-run":
3188
+ case "schedule":
3189
+ try {
3190
+ await schedulePipelineOnce(options);
3191
+ } catch (error) {
3192
+ printJson({
3193
+ status: "FAILED",
3194
+ schedule_created: false,
3195
+ error: {
3196
+ code: "INVALID_CLI_INPUT",
3197
+ message: error.message || "Invalid CLI input",
3198
+ retryable: false
3199
+ }
3200
+ });
3201
+ process.exitCode = 1;
3202
+ }
3203
+ break;
3204
+ case "schedule-status":
3205
+ case "scheduled-run":
3206
+ try {
3207
+ scheduleStatusCli(options);
3208
+ } catch (error) {
3209
+ printJson({
3210
+ status: "FAILED",
3211
+ error: {
3212
+ code: "INVALID_CLI_INPUT",
3213
+ message: error.message || "Invalid CLI input",
3214
+ retryable: false
3215
+ }
3216
+ });
3217
+ process.exitCode = 1;
3218
+ }
3219
+ break;
3049
3220
  case "list-jobs":
3050
3221
  case "jobs":
3051
3222
  case "recommend-jobs":
@@ -3189,11 +3360,13 @@ export async function runCli(argv = process.argv) {
3189
3360
  }
3190
3361
 
3191
3362
  export const __testables = {
3192
- buildRecommendJobListCliInput,
3193
- buildBossChatCliInput,
3194
- buildDefaultMcpArgs,
3195
- buildMcpLaunchConfig,
3196
- collectRuntimeDirectories,
3363
+ buildRecommendJobListCliInput,
3364
+ buildBossChatCliInput,
3365
+ buildDefaultMcpArgs,
3366
+ buildMcpLaunchConfig,
3367
+ ensureGlobalMcpWrapper,
3368
+ getGlobalMcpWrapperPath,
3369
+ collectRuntimeDirectories,
3197
3370
  ensureBossChatRuntimeReady: ensureBossChatRuntimeReadyLocal,
3198
3371
  ensureRuntimeDirectories,
3199
3372
  getBossChatCliRunTarget,
@@ -3206,6 +3379,8 @@ export const __testables = {
3206
3379
  resolveBossChatRuntimeLayout: resolveCdpBossChatRuntimeLayout,
3207
3380
  runBossChatCliCommand,
3208
3381
  preparePipelineOnce,
3382
+ schedulePipelineOnce,
3383
+ scheduleStatusCli,
3209
3384
  runPipelineOnce
3210
3385
  };
3211
3386
 
package/src/index.js CHANGED
@@ -39,6 +39,12 @@ import {
39
39
  startRecruitPipelineRunTool,
40
40
  validateRecruitPipelineArgs
41
41
  } from "./recruit-mcp.js";
42
+ import {
43
+ __setRecommendSchedulerSpawnForTests,
44
+ getRecommendScheduledRunTool,
45
+ runScheduledRecommendWorker,
46
+ scheduleRecommendPipelineRunTool
47
+ } from "./recommend-scheduler.js";
42
48
  import {
43
49
  __resetRecommendMcpStateForTests,
44
50
  __setRecommendMcpConnectorForTests,
@@ -90,6 +96,8 @@ const require = createRequire(import.meta.url);
90
96
  const { version: SERVER_VERSION } = require("../package.json");
91
97
 
92
98
  const TOOL_PREPARE_RUN = "prepare_recommend_pipeline_run";
99
+ const TOOL_SCHEDULE_RUN = "schedule_recommend_pipeline_run";
100
+ const TOOL_GET_SCHEDULED_RUN = "get_recommend_scheduled_run";
93
101
  const TOOL_START_RUN = "start_recommend_pipeline_run";
94
102
  const TOOL_GET_RUN = "get_recommend_pipeline_run";
95
103
  const TOOL_CANCEL_RUN = "cancel_recommend_pipeline_run";
@@ -1083,6 +1091,36 @@ function createListRecommendJobsInputSchema() {
1083
1091
  };
1084
1092
  }
1085
1093
 
1094
+ function createScheduleRunInputSchema() {
1095
+ const base = createRunInputSchema();
1096
+ return {
1097
+ ...base,
1098
+ properties: {
1099
+ ...base.properties,
1100
+ schedule_id: {
1101
+ type: "string",
1102
+ description: "可选,自定义定时任务 id;默认自动生成"
1103
+ },
1104
+ schedule_run_at: {
1105
+ type: "string",
1106
+ description: "ISO 时间字符串;到点后由 package-owned detached scheduler 启动已准备好的 payload"
1107
+ },
1108
+ schedule_delay_minutes: {
1109
+ type: "number",
1110
+ minimum: 0,
1111
+ description: "从现在开始延迟多少分钟后启动;适合 OpenClaw cron/定时任务设置"
1112
+ },
1113
+ schedule_delay_seconds: {
1114
+ type: "number",
1115
+ minimum: 0,
1116
+ description: "从现在开始延迟多少秒后启动;主要用于短延迟或测试"
1117
+ }
1118
+ },
1119
+ required: ["instruction"],
1120
+ additionalProperties: false
1121
+ };
1122
+ }
1123
+
1086
1124
  function createToolsSchema() {
1087
1125
  return [
1088
1126
  {
@@ -1095,6 +1133,23 @@ function createToolsSchema() {
1095
1133
  description: "只校验 Boss 推荐页流水线参数是否已可用于 cron/一次性任务;不会启动筛选任务。只有返回 READY/cron_ready=true 后才应创建定时任务。",
1096
1134
  inputSchema: createRunInputSchema()
1097
1135
  },
1136
+ {
1137
+ name: TOOL_SCHEDULE_RUN,
1138
+ description: "创建 package-owned Boss 推荐页定时任务:先校验 READY/cron_ready,再保存完整 payload,并由 detached scheduler 到点直接启动,不再依赖 AI harness 自己拼 shell cron。",
1139
+ inputSchema: createScheduleRunInputSchema()
1140
+ },
1141
+ {
1142
+ name: TOOL_GET_SCHEDULED_RUN,
1143
+ description: "查询 package-owned 推荐页定时任务状态;返回 schedule_id、worker 状态、到点后启动的 run_id 与运行快照。",
1144
+ inputSchema: {
1145
+ type: "object",
1146
+ properties: {
1147
+ schedule_id: { type: "string" }
1148
+ },
1149
+ required: ["schedule_id"],
1150
+ additionalProperties: false
1151
+ }
1152
+ },
1098
1153
  {
1099
1154
  name: TOOL_START_RUN,
1100
1155
  description: "异步启动 Boss 推荐页流水线(含同步门禁预检);只有在前置确认与页面就绪通过后才返回 run_id。",
@@ -2626,6 +2681,10 @@ async function handleRequest(message, workspaceRoot) {
2626
2681
  payload = await listRecommendJobsTool({ workspaceRoot, args });
2627
2682
  } else if (toolName === TOOL_PREPARE_RUN) {
2628
2683
  payload = prepareRecommendPipelineRunTool({ workspaceRoot, args });
2684
+ } else if (toolName === TOOL_SCHEDULE_RUN) {
2685
+ payload = await scheduleRecommendPipelineRunTool({ workspaceRoot, args });
2686
+ } else if (toolName === TOOL_GET_SCHEDULED_RUN) {
2687
+ payload = getRecommendScheduledRunTool({ args });
2629
2688
  } else if (toolName === TOOL_START_RUN) {
2630
2689
  payload = await handleStartRunTool({ workspaceRoot, args });
2631
2690
  } else if (toolName === TOOL_GET_RUN) {
@@ -2825,6 +2884,12 @@ export const __testables = {
2825
2884
  resetRecommendMcpStateForTests() {
2826
2885
  __resetRecommendMcpStateForTests();
2827
2886
  },
2887
+ setRecommendSchedulerSpawnForTests(nextImpl) {
2888
+ __setRecommendSchedulerSpawnForTests(nextImpl);
2889
+ },
2890
+ runScheduledRecommendWorkerForTests(options = {}) {
2891
+ return runScheduledRecommendWorker(options);
2892
+ },
2828
2893
  setChatMcpConnectorForTests(nextImpl) {
2829
2894
  forceChatInProcForTests = typeof nextImpl === "function";
2830
2895
  __setChatMcpConnectorForTests(nextImpl);