@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.22

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 (67) hide show
  1. package/README.md +5 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +8 -8
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2419 -2415
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1326 -1326
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +32 -32
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +571 -571
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +650 -650
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +504 -361
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +987 -854
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +13 -0
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +929 -915
  66. package/src/recommend-scheduler.js +496 -496
  67. package/src/recruit-mcp.js +2121 -2121
@@ -1,496 +1,496 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import process from "node:process";
4
- import { spawn } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
- import { getStateHome } from "./run-state.js";
7
- import {
8
- compactRecommendRunForStatus,
9
- getRecommendPipelineRunTool,
10
- prepareRecommendPipelineRunTool,
11
- startRecommendPipelineRunTool
12
- } from "./recommend-mcp.js";
13
-
14
- const SCHEDULE_WORKER_FLAG = "--schedule-worker";
15
- const SCHEDULE_ID_FLAG = "--schedule-id";
16
- const TERMINAL_SCHEDULE_STATES = new Set(["completed", "failed", "canceled"]);
17
- const TERMINAL_RUN_STATES = new Set(["completed", "failed", "canceled"]);
18
-
19
- let spawnProcessImpl = spawn;
20
-
21
- function normalizeText(value) {
22
- return String(value || "").replace(/\s+/g, " ").trim();
23
- }
24
-
25
- function clonePlain(value, fallback = null) {
26
- try {
27
- return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
28
- } catch {
29
- return fallback;
30
- }
31
- }
32
-
33
- function nowIso() {
34
- return new Date().toISOString();
35
- }
36
-
37
- function sleep(ms) {
38
- return new Promise((resolve) => setTimeout(resolve, ms));
39
- }
40
-
41
- function parseNonNegativeNumber(raw, fallback = null) {
42
- if (raw === undefined || raw === null || raw === "") return fallback;
43
- const parsed = Number(raw);
44
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
45
- }
46
-
47
- function parseRunAt(args = {}) {
48
- const direct = normalizeText(args.schedule_run_at || args.scheduleRunAt || args.run_at || args.runAt);
49
- if (direct) {
50
- const timestamp = Date.parse(direct);
51
- if (!Number.isFinite(timestamp)) {
52
- return {
53
- ok: false,
54
- error: `Invalid schedule_run_at: ${direct}`
55
- };
56
- }
57
- return {
58
- ok: true,
59
- runAtMs: timestamp,
60
- source: "schedule_run_at"
61
- };
62
- }
63
-
64
- const delaySeconds = parseNonNegativeNumber(args.schedule_delay_seconds ?? args.scheduleDelaySeconds, null);
65
- if (delaySeconds !== null) {
66
- return {
67
- ok: true,
68
- runAtMs: Date.now() + Math.round(delaySeconds * 1000),
69
- source: "schedule_delay_seconds"
70
- };
71
- }
72
-
73
- const delayMinutes = parseNonNegativeNumber(args.schedule_delay_minutes ?? args.scheduleDelayMinutes, null);
74
- if (delayMinutes !== null) {
75
- return {
76
- ok: true,
77
- runAtMs: Date.now() + Math.round(delayMinutes * 60 * 1000),
78
- source: "schedule_delay_minutes"
79
- };
80
- }
81
-
82
- return {
83
- ok: false,
84
- error: "schedule_run_at or schedule_delay_minutes/schedule_delay_seconds is required"
85
- };
86
- }
87
-
88
- function safeIdPart(value) {
89
- return normalizeText(value).replace(/[^a-zA-Z0-9_.-]/g, "_");
90
- }
91
-
92
- function createScheduleId(raw = "") {
93
- const requested = safeIdPart(raw);
94
- if (requested) return requested;
95
- const suffix = Math.random().toString(36).slice(2, 10);
96
- return `mcp_recommend_schedule_${Date.now().toString(36)}_${suffix}`;
97
- }
98
-
99
- function getSchedulesDir() {
100
- return path.join(getStateHome(), "schedules");
101
- }
102
-
103
- function getScheduleArtifacts(scheduleId) {
104
- const id = safeIdPart(scheduleId);
105
- if (!id) throw new Error("schedule_id is required");
106
- return {
107
- schedule_path: path.join(getSchedulesDir(), `${id}.json`),
108
- worker_stdout_path: path.join(getSchedulesDir(), `${id}.worker.stdout.log`),
109
- worker_stderr_path: path.join(getSchedulesDir(), `${id}.worker.stderr.log`)
110
- };
111
- }
112
-
113
- function writeJsonAtomic(filePath, payload) {
114
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
115
- const tempPath = `${filePath}.tmp`;
116
- fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
117
- fs.renameSync(tempPath, filePath);
118
- return payload;
119
- }
120
-
121
- function readJsonFile(filePath) {
122
- try {
123
- const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
124
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
125
- } catch {
126
- return null;
127
- }
128
- }
129
-
130
- function readSchedule(scheduleId) {
131
- const artifacts = getScheduleArtifacts(scheduleId);
132
- return readJsonFile(artifacts.schedule_path);
133
- }
134
-
135
- function writeSchedule(scheduleId, patch) {
136
- const artifacts = getScheduleArtifacts(scheduleId);
137
- const current = readJsonFile(artifacts.schedule_path) || {};
138
- return writeJsonAtomic(artifacts.schedule_path, {
139
- ...current,
140
- ...patch,
141
- schedule_id: scheduleId,
142
- updated_at: nowIso()
143
- });
144
- }
145
-
146
- function compactScheduleForStatus(schedule) {
147
- if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) return schedule || null;
148
- const compact = clonePlain(schedule, schedule);
149
- if (compact.run) compact.run = compactRecommendRunForStatus(compact.run);
150
- if (compact.launch_payload?.run) {
151
- compact.launch_payload = {
152
- ...compact.launch_payload,
153
- run: compactRecommendRunForStatus(compact.launch_payload.run)
154
- };
155
- }
156
- return compact;
157
- }
158
-
159
- function isProcessAlive(pid) {
160
- if (!Number.isInteger(pid) || pid <= 0) return false;
161
- try {
162
- process.kill(pid, 0);
163
- return true;
164
- } catch {
165
- return false;
166
- }
167
- }
168
-
169
- function stripScheduleArgs(args = {}) {
170
- const cloned = clonePlain(args, {});
171
- for (const key of [
172
- "schedule_id",
173
- "scheduleId",
174
- "schedule_run_at",
175
- "scheduleRunAt",
176
- "run_at",
177
- "runAt",
178
- "schedule_delay_seconds",
179
- "scheduleDelaySeconds",
180
- "schedule_delay_minutes",
181
- "scheduleDelayMinutes"
182
- ]) {
183
- delete cloned[key];
184
- }
185
- return cloned;
186
- }
187
-
188
- function buildFailedSchedulePayload(error, extra = {}) {
189
- return {
190
- status: "FAILED",
191
- schedule_created: false,
192
- error: {
193
- code: "RECOMMEND_SCHEDULE_FAILED",
194
- message: error?.message || String(error || "Unable to schedule recommend run"),
195
- retryable: true
196
- },
197
- ...extra
198
- };
199
- }
200
-
201
- function launchScheduleWorker(scheduleId) {
202
- const artifacts = getScheduleArtifacts(scheduleId);
203
- fs.mkdirSync(path.dirname(artifacts.worker_stdout_path), { recursive: true });
204
- const stdoutFd = fs.openSync(artifacts.worker_stdout_path, "a");
205
- const stderrFd = fs.openSync(artifacts.worker_stderr_path, "a");
206
- let child;
207
- try {
208
- child = spawnProcessImpl(process.execPath, [
209
- thisFilePath,
210
- SCHEDULE_WORKER_FLAG,
211
- SCHEDULE_ID_FLAG,
212
- scheduleId
213
- ], {
214
- cwd: process.cwd(),
215
- detached: true,
216
- stdio: ["ignore", stdoutFd, stderrFd],
217
- windowsHide: true,
218
- env: process.env
219
- });
220
- } finally {
221
- fs.closeSync(stdoutFd);
222
- fs.closeSync(stderrFd);
223
- }
224
- if (typeof child?.unref === "function") child.unref();
225
- return {
226
- pid: child.pid || null,
227
- stdoutPath: artifacts.worker_stdout_path,
228
- stderrPath: artifacts.worker_stderr_path
229
- };
230
- }
231
-
232
- export async function scheduleRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
233
- const runArgs = stripScheduleArgs(args);
234
- const prepared = prepareRecommendPipelineRunTool({ workspaceRoot, args: runArgs });
235
- if (prepared.status !== "READY" || prepared.cron_ready !== true) {
236
- return {
237
- ...prepared,
238
- status: prepared.status || "FAILED",
239
- schedule_created: false,
240
- cron_ready: false,
241
- message: "Recommend schedule was not created because the run payload is not READY."
242
- };
243
- }
244
-
245
- const due = parseRunAt(args);
246
- if (!due.ok) {
247
- return {
248
- status: "FAILED",
249
- schedule_created: false,
250
- cron_ready: true,
251
- error: {
252
- code: "INVALID_SCHEDULE_TIME",
253
- message: due.error,
254
- retryable: false
255
- },
256
- prepare: prepared
257
- };
258
- }
259
-
260
- const scheduleId = createScheduleId(args.schedule_id || args.scheduleId);
261
- const artifacts = getScheduleArtifacts(scheduleId);
262
- const runAtIso = new Date(due.runAtMs).toISOString();
263
- const createdAt = nowIso();
264
- try {
265
- writeJsonAtomic(artifacts.schedule_path, {
266
- schedule_id: scheduleId,
267
- state: "scheduled",
268
- status: "scheduled",
269
- created_at: createdAt,
270
- updated_at: createdAt,
271
- run_at: runAtIso,
272
- run_at_ms: due.runAtMs,
273
- time_source: due.source,
274
- workspace_root: path.resolve(workspaceRoot || process.cwd()),
275
- args: runArgs,
276
- prepare: prepared,
277
- worker_stdout_path: artifacts.worker_stdout_path,
278
- worker_stderr_path: artifacts.worker_stderr_path,
279
- pid: null,
280
- run_id: null,
281
- run: null,
282
- error: null
283
- });
284
- } catch (error) {
285
- return buildFailedSchedulePayload(error, { prepare: prepared });
286
- }
287
-
288
- let worker;
289
- try {
290
- worker = launchScheduleWorker(scheduleId);
291
- } catch (error) {
292
- writeSchedule(scheduleId, {
293
- state: "failed",
294
- status: "failed",
295
- error: {
296
- code: "SCHEDULE_WORKER_LAUNCH_FAILED",
297
- message: error?.message || String(error || "Unable to launch schedule worker"),
298
- retryable: true
299
- }
300
- });
301
- return buildFailedSchedulePayload(error, {
302
- schedule_created: true,
303
- schedule_id: scheduleId,
304
- schedule: readSchedule(scheduleId),
305
- prepare: prepared
306
- });
307
- }
308
-
309
- const schedule = writeSchedule(scheduleId, {
310
- state: "scheduled",
311
- status: "scheduled",
312
- pid: worker.pid,
313
- worker_stdout_path: worker.stdoutPath,
314
- worker_stderr_path: worker.stderrPath
315
- });
316
-
317
- return {
318
- status: "SCHEDULED",
319
- schedule_created: true,
320
- cron_ready: true,
321
- schedule_id: scheduleId,
322
- run_at: runAtIso,
323
- run_at_ms: due.runAtMs,
324
- worker_pid: worker.pid,
325
- worker_stdout_path: worker.stdoutPath,
326
- worker_stderr_path: worker.stderrPath,
327
- schedule,
328
- prepare: prepared,
329
- message: "Recommend run schedule created. The package-owned detached scheduler will start the prepared payload at run_at."
330
- };
331
- }
332
-
333
- export function getRecommendScheduledRunTool({ args = {} } = {}) {
334
- const scheduleId = safeIdPart(args.schedule_id || args.scheduleId);
335
- if (!scheduleId) {
336
- return {
337
- status: "FAILED",
338
- error: {
339
- code: "INVALID_SCHEDULE_ID",
340
- message: "schedule_id is required",
341
- retryable: false
342
- }
343
- };
344
- }
345
- const schedule = readSchedule(scheduleId);
346
- if (!schedule) {
347
- return {
348
- status: "FAILED",
349
- error: {
350
- code: "SCHEDULE_NOT_FOUND",
351
- message: `schedule_id=${scheduleId} not found`,
352
- retryable: false
353
- }
354
- };
355
- }
356
- let next = schedule;
357
- if (!TERMINAL_SCHEDULE_STATES.has(normalizeText(schedule.state || schedule.status)) && schedule.pid && !isProcessAlive(schedule.pid)) {
358
- next = writeSchedule(scheduleId, {
359
- state: "failed",
360
- status: "failed",
361
- completed_at: nowIso(),
362
- error: {
363
- code: "SCHEDULE_WORKER_EXITED",
364
- message: `Scheduled worker process exited before reaching a terminal state (pid=${schedule.pid}).`,
365
- retryable: true
366
- }
367
- });
368
- }
369
- return {
370
- status: "OK",
371
- schedule_id: scheduleId,
372
- schedule: compactScheduleForStatus(next)
373
- };
374
- }
375
-
376
- export async function runScheduledRecommendWorker({ scheduleId }) {
377
- const normalizedScheduleId = safeIdPart(scheduleId);
378
- if (!normalizedScheduleId) return { ok: false, error: "schedule_id is required" };
379
- let schedule = readSchedule(normalizedScheduleId);
380
- if (!schedule) return { ok: false, error: `schedule_id=${normalizedScheduleId} not found` };
381
- const runAtMs = Number(schedule.run_at_ms);
382
- if (!Number.isFinite(runAtMs)) {
383
- writeSchedule(normalizedScheduleId, {
384
- state: "failed",
385
- status: "failed",
386
- completed_at: nowIso(),
387
- error: {
388
- code: "INVALID_SCHEDULE_STATE",
389
- message: "schedule is missing a valid run_at_ms",
390
- retryable: false
391
- }
392
- });
393
- return { ok: false, error: "schedule is missing a valid run_at_ms" };
394
- }
395
-
396
- schedule = writeSchedule(normalizedScheduleId, {
397
- state: "waiting",
398
- status: "waiting",
399
- pid: process.pid,
400
- worker_started_at: nowIso()
401
- });
402
-
403
- while (Date.now() < runAtMs) {
404
- await sleep(Math.min(30_000, Math.max(50, runAtMs - Date.now())));
405
- const latest = readSchedule(normalizedScheduleId);
406
- if (normalizeText(latest?.state) === "canceled") return { ok: true, canceled: true };
407
- }
408
-
409
- writeSchedule(normalizedScheduleId, {
410
- state: "launching",
411
- status: "launching",
412
- launch_started_at: nowIso()
413
- });
414
-
415
- const started = await startRecommendPipelineRunTool({
416
- workspaceRoot: schedule.workspace_root,
417
- args: clonePlain(schedule.args, {})
418
- });
419
- if (started.status !== "ACCEPTED") {
420
- writeSchedule(normalizedScheduleId, {
421
- state: "failed",
422
- status: "failed",
423
- completed_at: nowIso(),
424
- launch_payload: started,
425
- error: started.error || {
426
- code: "RECOMMEND_START_NOT_ACCEPTED",
427
- message: started.status || "start_recommend_pipeline_run did not return ACCEPTED",
428
- retryable: true
429
- }
430
- });
431
- return { ok: false, error: started.error?.message || started.status || "not accepted" };
432
- }
433
-
434
- writeSchedule(normalizedScheduleId, {
435
- state: "running",
436
- status: "running",
437
- run_id: started.run_id,
438
- run: started.run || null,
439
- launch_payload: started,
440
- launched_at: nowIso()
441
- });
442
-
443
- while (true) {
444
- const payload = getRecommendPipelineRunTool({ args: { run_id: started.run_id } });
445
- const runState = normalizeText(payload?.run?.state || payload?.run?.status);
446
- writeSchedule(normalizedScheduleId, {
447
- state: runState && TERMINAL_RUN_STATES.has(runState) ? runState : "running",
448
- status: runState && TERMINAL_RUN_STATES.has(runState) ? runState : "running",
449
- run_id: started.run_id,
450
- run: payload?.run || null,
451
- last_poll_at: nowIso(),
452
- completed_at: runState && TERMINAL_RUN_STATES.has(runState) ? nowIso() : undefined,
453
- error: runState === "failed" ? (payload?.run?.error || payload?.error || null) : null
454
- });
455
- if (TERMINAL_RUN_STATES.has(runState)) break;
456
- await sleep(1000);
457
- }
458
- return { ok: true, run_id: started.run_id };
459
- }
460
-
461
- export function __setRecommendSchedulerSpawnForTests(nextImpl) {
462
- spawnProcessImpl = typeof nextImpl === "function" ? nextImpl : spawn;
463
- }
464
-
465
- function parseScheduleWorkerOptions(argv = process.argv.slice(2)) {
466
- if (!Array.isArray(argv) || !argv.includes(SCHEDULE_WORKER_FLAG)) return null;
467
- const idIndex = argv.indexOf(SCHEDULE_ID_FLAG);
468
- return {
469
- scheduleId: idIndex >= 0 ? normalizeText(argv[idIndex + 1]) : ""
470
- };
471
- }
472
-
473
- const thisFilePath = fileURLToPath(import.meta.url);
474
- if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
475
- const options = parseScheduleWorkerOptions(process.argv.slice(2));
476
- if (options) {
477
- runScheduledRecommendWorker(options).then((result) => {
478
- if (!result?.ok) process.exitCode = 1;
479
- }).catch((error) => {
480
- try {
481
- writeSchedule(options.scheduleId, {
482
- state: "failed",
483
- status: "failed",
484
- completed_at: nowIso(),
485
- error: {
486
- code: "SCHEDULE_WORKER_UNHANDLED_ERROR",
487
- message: error?.message || String(error || "schedule worker failed"),
488
- retryable: true
489
- }
490
- });
491
- } catch {}
492
- console.error("[boss-recommend-mcp] scheduled recommend worker failed", error);
493
- process.exitCode = 1;
494
- });
495
- }
496
- }
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { spawn } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { getStateHome } from "./run-state.js";
7
+ import {
8
+ compactRecommendRunForStatus,
9
+ getRecommendPipelineRunTool,
10
+ prepareRecommendPipelineRunTool,
11
+ startRecommendPipelineRunTool
12
+ } from "./recommend-mcp.js";
13
+
14
+ const SCHEDULE_WORKER_FLAG = "--schedule-worker";
15
+ const SCHEDULE_ID_FLAG = "--schedule-id";
16
+ const TERMINAL_SCHEDULE_STATES = new Set(["completed", "failed", "canceled"]);
17
+ const TERMINAL_RUN_STATES = new Set(["completed", "failed", "canceled"]);
18
+
19
+ let spawnProcessImpl = spawn;
20
+
21
+ function normalizeText(value) {
22
+ return String(value || "").replace(/\s+/g, " ").trim();
23
+ }
24
+
25
+ function clonePlain(value, fallback = null) {
26
+ try {
27
+ return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
28
+ } catch {
29
+ return fallback;
30
+ }
31
+ }
32
+
33
+ function nowIso() {
34
+ return new Date().toISOString();
35
+ }
36
+
37
+ function sleep(ms) {
38
+ return new Promise((resolve) => setTimeout(resolve, ms));
39
+ }
40
+
41
+ function parseNonNegativeNumber(raw, fallback = null) {
42
+ if (raw === undefined || raw === null || raw === "") return fallback;
43
+ const parsed = Number(raw);
44
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
45
+ }
46
+
47
+ function parseRunAt(args = {}) {
48
+ const direct = normalizeText(args.schedule_run_at || args.scheduleRunAt || args.run_at || args.runAt);
49
+ if (direct) {
50
+ const timestamp = Date.parse(direct);
51
+ if (!Number.isFinite(timestamp)) {
52
+ return {
53
+ ok: false,
54
+ error: `Invalid schedule_run_at: ${direct}`
55
+ };
56
+ }
57
+ return {
58
+ ok: true,
59
+ runAtMs: timestamp,
60
+ source: "schedule_run_at"
61
+ };
62
+ }
63
+
64
+ const delaySeconds = parseNonNegativeNumber(args.schedule_delay_seconds ?? args.scheduleDelaySeconds, null);
65
+ if (delaySeconds !== null) {
66
+ return {
67
+ ok: true,
68
+ runAtMs: Date.now() + Math.round(delaySeconds * 1000),
69
+ source: "schedule_delay_seconds"
70
+ };
71
+ }
72
+
73
+ const delayMinutes = parseNonNegativeNumber(args.schedule_delay_minutes ?? args.scheduleDelayMinutes, null);
74
+ if (delayMinutes !== null) {
75
+ return {
76
+ ok: true,
77
+ runAtMs: Date.now() + Math.round(delayMinutes * 60 * 1000),
78
+ source: "schedule_delay_minutes"
79
+ };
80
+ }
81
+
82
+ return {
83
+ ok: false,
84
+ error: "schedule_run_at or schedule_delay_minutes/schedule_delay_seconds is required"
85
+ };
86
+ }
87
+
88
+ function safeIdPart(value) {
89
+ return normalizeText(value).replace(/[^a-zA-Z0-9_.-]/g, "_");
90
+ }
91
+
92
+ function createScheduleId(raw = "") {
93
+ const requested = safeIdPart(raw);
94
+ if (requested) return requested;
95
+ const suffix = Math.random().toString(36).slice(2, 10);
96
+ return `mcp_recommend_schedule_${Date.now().toString(36)}_${suffix}`;
97
+ }
98
+
99
+ function getSchedulesDir() {
100
+ return path.join(getStateHome(), "schedules");
101
+ }
102
+
103
+ function getScheduleArtifacts(scheduleId) {
104
+ const id = safeIdPart(scheduleId);
105
+ if (!id) throw new Error("schedule_id is required");
106
+ return {
107
+ schedule_path: path.join(getSchedulesDir(), `${id}.json`),
108
+ worker_stdout_path: path.join(getSchedulesDir(), `${id}.worker.stdout.log`),
109
+ worker_stderr_path: path.join(getSchedulesDir(), `${id}.worker.stderr.log`)
110
+ };
111
+ }
112
+
113
+ function writeJsonAtomic(filePath, payload) {
114
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
115
+ const tempPath = `${filePath}.tmp`;
116
+ fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
117
+ fs.renameSync(tempPath, filePath);
118
+ return payload;
119
+ }
120
+
121
+ function readJsonFile(filePath) {
122
+ try {
123
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
124
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ function readSchedule(scheduleId) {
131
+ const artifacts = getScheduleArtifacts(scheduleId);
132
+ return readJsonFile(artifacts.schedule_path);
133
+ }
134
+
135
+ function writeSchedule(scheduleId, patch) {
136
+ const artifacts = getScheduleArtifacts(scheduleId);
137
+ const current = readJsonFile(artifacts.schedule_path) || {};
138
+ return writeJsonAtomic(artifacts.schedule_path, {
139
+ ...current,
140
+ ...patch,
141
+ schedule_id: scheduleId,
142
+ updated_at: nowIso()
143
+ });
144
+ }
145
+
146
+ function compactScheduleForStatus(schedule) {
147
+ if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) return schedule || null;
148
+ const compact = clonePlain(schedule, schedule);
149
+ if (compact.run) compact.run = compactRecommendRunForStatus(compact.run);
150
+ if (compact.launch_payload?.run) {
151
+ compact.launch_payload = {
152
+ ...compact.launch_payload,
153
+ run: compactRecommendRunForStatus(compact.launch_payload.run)
154
+ };
155
+ }
156
+ return compact;
157
+ }
158
+
159
+ function isProcessAlive(pid) {
160
+ if (!Number.isInteger(pid) || pid <= 0) return false;
161
+ try {
162
+ process.kill(pid, 0);
163
+ return true;
164
+ } catch {
165
+ return false;
166
+ }
167
+ }
168
+
169
+ function stripScheduleArgs(args = {}) {
170
+ const cloned = clonePlain(args, {});
171
+ for (const key of [
172
+ "schedule_id",
173
+ "scheduleId",
174
+ "schedule_run_at",
175
+ "scheduleRunAt",
176
+ "run_at",
177
+ "runAt",
178
+ "schedule_delay_seconds",
179
+ "scheduleDelaySeconds",
180
+ "schedule_delay_minutes",
181
+ "scheduleDelayMinutes"
182
+ ]) {
183
+ delete cloned[key];
184
+ }
185
+ return cloned;
186
+ }
187
+
188
+ function buildFailedSchedulePayload(error, extra = {}) {
189
+ return {
190
+ status: "FAILED",
191
+ schedule_created: false,
192
+ error: {
193
+ code: "RECOMMEND_SCHEDULE_FAILED",
194
+ message: error?.message || String(error || "Unable to schedule recommend run"),
195
+ retryable: true
196
+ },
197
+ ...extra
198
+ };
199
+ }
200
+
201
+ function launchScheduleWorker(scheduleId) {
202
+ const artifacts = getScheduleArtifacts(scheduleId);
203
+ fs.mkdirSync(path.dirname(artifacts.worker_stdout_path), { recursive: true });
204
+ const stdoutFd = fs.openSync(artifacts.worker_stdout_path, "a");
205
+ const stderrFd = fs.openSync(artifacts.worker_stderr_path, "a");
206
+ let child;
207
+ try {
208
+ child = spawnProcessImpl(process.execPath, [
209
+ thisFilePath,
210
+ SCHEDULE_WORKER_FLAG,
211
+ SCHEDULE_ID_FLAG,
212
+ scheduleId
213
+ ], {
214
+ cwd: process.cwd(),
215
+ detached: true,
216
+ stdio: ["ignore", stdoutFd, stderrFd],
217
+ windowsHide: true,
218
+ env: process.env
219
+ });
220
+ } finally {
221
+ fs.closeSync(stdoutFd);
222
+ fs.closeSync(stderrFd);
223
+ }
224
+ if (typeof child?.unref === "function") child.unref();
225
+ return {
226
+ pid: child.pid || null,
227
+ stdoutPath: artifacts.worker_stdout_path,
228
+ stderrPath: artifacts.worker_stderr_path
229
+ };
230
+ }
231
+
232
+ export async function scheduleRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
233
+ const runArgs = stripScheduleArgs(args);
234
+ const prepared = prepareRecommendPipelineRunTool({ workspaceRoot, args: runArgs });
235
+ if (prepared.status !== "READY" || prepared.cron_ready !== true) {
236
+ return {
237
+ ...prepared,
238
+ status: prepared.status || "FAILED",
239
+ schedule_created: false,
240
+ cron_ready: false,
241
+ message: "Recommend schedule was not created because the run payload is not READY."
242
+ };
243
+ }
244
+
245
+ const due = parseRunAt(args);
246
+ if (!due.ok) {
247
+ return {
248
+ status: "FAILED",
249
+ schedule_created: false,
250
+ cron_ready: true,
251
+ error: {
252
+ code: "INVALID_SCHEDULE_TIME",
253
+ message: due.error,
254
+ retryable: false
255
+ },
256
+ prepare: prepared
257
+ };
258
+ }
259
+
260
+ const scheduleId = createScheduleId(args.schedule_id || args.scheduleId);
261
+ const artifacts = getScheduleArtifacts(scheduleId);
262
+ const runAtIso = new Date(due.runAtMs).toISOString();
263
+ const createdAt = nowIso();
264
+ try {
265
+ writeJsonAtomic(artifacts.schedule_path, {
266
+ schedule_id: scheduleId,
267
+ state: "scheduled",
268
+ status: "scheduled",
269
+ created_at: createdAt,
270
+ updated_at: createdAt,
271
+ run_at: runAtIso,
272
+ run_at_ms: due.runAtMs,
273
+ time_source: due.source,
274
+ workspace_root: path.resolve(workspaceRoot || process.cwd()),
275
+ args: runArgs,
276
+ prepare: prepared,
277
+ worker_stdout_path: artifacts.worker_stdout_path,
278
+ worker_stderr_path: artifacts.worker_stderr_path,
279
+ pid: null,
280
+ run_id: null,
281
+ run: null,
282
+ error: null
283
+ });
284
+ } catch (error) {
285
+ return buildFailedSchedulePayload(error, { prepare: prepared });
286
+ }
287
+
288
+ let worker;
289
+ try {
290
+ worker = launchScheduleWorker(scheduleId);
291
+ } catch (error) {
292
+ writeSchedule(scheduleId, {
293
+ state: "failed",
294
+ status: "failed",
295
+ error: {
296
+ code: "SCHEDULE_WORKER_LAUNCH_FAILED",
297
+ message: error?.message || String(error || "Unable to launch schedule worker"),
298
+ retryable: true
299
+ }
300
+ });
301
+ return buildFailedSchedulePayload(error, {
302
+ schedule_created: true,
303
+ schedule_id: scheduleId,
304
+ schedule: readSchedule(scheduleId),
305
+ prepare: prepared
306
+ });
307
+ }
308
+
309
+ const schedule = writeSchedule(scheduleId, {
310
+ state: "scheduled",
311
+ status: "scheduled",
312
+ pid: worker.pid,
313
+ worker_stdout_path: worker.stdoutPath,
314
+ worker_stderr_path: worker.stderrPath
315
+ });
316
+
317
+ return {
318
+ status: "SCHEDULED",
319
+ schedule_created: true,
320
+ cron_ready: true,
321
+ schedule_id: scheduleId,
322
+ run_at: runAtIso,
323
+ run_at_ms: due.runAtMs,
324
+ worker_pid: worker.pid,
325
+ worker_stdout_path: worker.stdoutPath,
326
+ worker_stderr_path: worker.stderrPath,
327
+ schedule,
328
+ prepare: prepared,
329
+ message: "Recommend run schedule created. The package-owned detached scheduler will start the prepared payload at run_at."
330
+ };
331
+ }
332
+
333
+ export function getRecommendScheduledRunTool({ args = {} } = {}) {
334
+ const scheduleId = safeIdPart(args.schedule_id || args.scheduleId);
335
+ if (!scheduleId) {
336
+ return {
337
+ status: "FAILED",
338
+ error: {
339
+ code: "INVALID_SCHEDULE_ID",
340
+ message: "schedule_id is required",
341
+ retryable: false
342
+ }
343
+ };
344
+ }
345
+ const schedule = readSchedule(scheduleId);
346
+ if (!schedule) {
347
+ return {
348
+ status: "FAILED",
349
+ error: {
350
+ code: "SCHEDULE_NOT_FOUND",
351
+ message: `schedule_id=${scheduleId} not found`,
352
+ retryable: false
353
+ }
354
+ };
355
+ }
356
+ let next = schedule;
357
+ if (!TERMINAL_SCHEDULE_STATES.has(normalizeText(schedule.state || schedule.status)) && schedule.pid && !isProcessAlive(schedule.pid)) {
358
+ next = writeSchedule(scheduleId, {
359
+ state: "failed",
360
+ status: "failed",
361
+ completed_at: nowIso(),
362
+ error: {
363
+ code: "SCHEDULE_WORKER_EXITED",
364
+ message: `Scheduled worker process exited before reaching a terminal state (pid=${schedule.pid}).`,
365
+ retryable: true
366
+ }
367
+ });
368
+ }
369
+ return {
370
+ status: "OK",
371
+ schedule_id: scheduleId,
372
+ schedule: compactScheduleForStatus(next)
373
+ };
374
+ }
375
+
376
+ export async function runScheduledRecommendWorker({ scheduleId }) {
377
+ const normalizedScheduleId = safeIdPart(scheduleId);
378
+ if (!normalizedScheduleId) return { ok: false, error: "schedule_id is required" };
379
+ let schedule = readSchedule(normalizedScheduleId);
380
+ if (!schedule) return { ok: false, error: `schedule_id=${normalizedScheduleId} not found` };
381
+ const runAtMs = Number(schedule.run_at_ms);
382
+ if (!Number.isFinite(runAtMs)) {
383
+ writeSchedule(normalizedScheduleId, {
384
+ state: "failed",
385
+ status: "failed",
386
+ completed_at: nowIso(),
387
+ error: {
388
+ code: "INVALID_SCHEDULE_STATE",
389
+ message: "schedule is missing a valid run_at_ms",
390
+ retryable: false
391
+ }
392
+ });
393
+ return { ok: false, error: "schedule is missing a valid run_at_ms" };
394
+ }
395
+
396
+ schedule = writeSchedule(normalizedScheduleId, {
397
+ state: "waiting",
398
+ status: "waiting",
399
+ pid: process.pid,
400
+ worker_started_at: nowIso()
401
+ });
402
+
403
+ while (Date.now() < runAtMs) {
404
+ await sleep(Math.min(30_000, Math.max(50, runAtMs - Date.now())));
405
+ const latest = readSchedule(normalizedScheduleId);
406
+ if (normalizeText(latest?.state) === "canceled") return { ok: true, canceled: true };
407
+ }
408
+
409
+ writeSchedule(normalizedScheduleId, {
410
+ state: "launching",
411
+ status: "launching",
412
+ launch_started_at: nowIso()
413
+ });
414
+
415
+ const started = await startRecommendPipelineRunTool({
416
+ workspaceRoot: schedule.workspace_root,
417
+ args: clonePlain(schedule.args, {})
418
+ });
419
+ if (started.status !== "ACCEPTED") {
420
+ writeSchedule(normalizedScheduleId, {
421
+ state: "failed",
422
+ status: "failed",
423
+ completed_at: nowIso(),
424
+ launch_payload: started,
425
+ error: started.error || {
426
+ code: "RECOMMEND_START_NOT_ACCEPTED",
427
+ message: started.status || "start_recommend_pipeline_run did not return ACCEPTED",
428
+ retryable: true
429
+ }
430
+ });
431
+ return { ok: false, error: started.error?.message || started.status || "not accepted" };
432
+ }
433
+
434
+ writeSchedule(normalizedScheduleId, {
435
+ state: "running",
436
+ status: "running",
437
+ run_id: started.run_id,
438
+ run: started.run || null,
439
+ launch_payload: started,
440
+ launched_at: nowIso()
441
+ });
442
+
443
+ while (true) {
444
+ const payload = getRecommendPipelineRunTool({ args: { run_id: started.run_id } });
445
+ const runState = normalizeText(payload?.run?.state || payload?.run?.status);
446
+ writeSchedule(normalizedScheduleId, {
447
+ state: runState && TERMINAL_RUN_STATES.has(runState) ? runState : "running",
448
+ status: runState && TERMINAL_RUN_STATES.has(runState) ? runState : "running",
449
+ run_id: started.run_id,
450
+ run: payload?.run || null,
451
+ last_poll_at: nowIso(),
452
+ completed_at: runState && TERMINAL_RUN_STATES.has(runState) ? nowIso() : undefined,
453
+ error: runState === "failed" ? (payload?.run?.error || payload?.error || null) : null
454
+ });
455
+ if (TERMINAL_RUN_STATES.has(runState)) break;
456
+ await sleep(1000);
457
+ }
458
+ return { ok: true, run_id: started.run_id };
459
+ }
460
+
461
+ export function __setRecommendSchedulerSpawnForTests(nextImpl) {
462
+ spawnProcessImpl = typeof nextImpl === "function" ? nextImpl : spawn;
463
+ }
464
+
465
+ function parseScheduleWorkerOptions(argv = process.argv.slice(2)) {
466
+ if (!Array.isArray(argv) || !argv.includes(SCHEDULE_WORKER_FLAG)) return null;
467
+ const idIndex = argv.indexOf(SCHEDULE_ID_FLAG);
468
+ return {
469
+ scheduleId: idIndex >= 0 ? normalizeText(argv[idIndex + 1]) : ""
470
+ };
471
+ }
472
+
473
+ const thisFilePath = fileURLToPath(import.meta.url);
474
+ if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
475
+ const options = parseScheduleWorkerOptions(process.argv.slice(2));
476
+ if (options) {
477
+ runScheduledRecommendWorker(options).then((result) => {
478
+ if (!result?.ok) process.exitCode = 1;
479
+ }).catch((error) => {
480
+ try {
481
+ writeSchedule(options.scheduleId, {
482
+ state: "failed",
483
+ status: "failed",
484
+ completed_at: nowIso(),
485
+ error: {
486
+ code: "SCHEDULE_WORKER_UNHANDLED_ERROR",
487
+ message: error?.message || String(error || "schedule worker failed"),
488
+ retryable: true
489
+ }
490
+ });
491
+ } catch {}
492
+ console.error("[boss-recommend-mcp] scheduled recommend worker failed", error);
493
+ process.exitCode = 1;
494
+ });
495
+ }
496
+ }