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