@juspay/neurolink 9.39.0 → 9.41.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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +445 -431
- package/dist/cli/commands/task.d.ts +56 -0
- package/dist/cli/commands/task.js +835 -0
- package/dist/cli/parser.js +4 -1
- package/dist/lib/neurolink.d.ts +22 -1
- package/dist/lib/neurolink.js +195 -14
- package/dist/lib/tasks/backends/bullmqBackend.d.ts +32 -0
- package/dist/lib/tasks/backends/bullmqBackend.js +189 -0
- package/dist/lib/tasks/backends/nodeTimeoutBackend.d.ts +27 -0
- package/dist/lib/tasks/backends/nodeTimeoutBackend.js +141 -0
- package/dist/lib/tasks/backends/taskBackendRegistry.d.ts +31 -0
- package/dist/lib/tasks/backends/taskBackendRegistry.js +66 -0
- package/dist/lib/tasks/errors.d.ts +31 -0
- package/dist/lib/tasks/errors.js +18 -0
- package/dist/lib/tasks/store/fileTaskStore.d.ts +43 -0
- package/dist/lib/tasks/store/fileTaskStore.js +179 -0
- package/dist/lib/tasks/store/redisTaskStore.d.ts +42 -0
- package/dist/lib/tasks/store/redisTaskStore.js +189 -0
- package/dist/lib/tasks/taskExecutor.d.ts +21 -0
- package/dist/lib/tasks/taskExecutor.js +166 -0
- package/dist/lib/tasks/taskManager.d.ts +60 -0
- package/dist/lib/tasks/taskManager.js +393 -0
- package/dist/lib/tasks/tools/taskTools.d.ts +135 -0
- package/dist/lib/tasks/tools/taskTools.js +274 -0
- package/dist/lib/types/configTypes.d.ts +3 -0
- package/dist/lib/types/generateTypes.d.ts +42 -0
- package/dist/lib/types/index.d.ts +2 -1
- package/dist/lib/types/streamTypes.d.ts +7 -0
- package/dist/lib/types/taskTypes.d.ts +275 -0
- package/dist/lib/types/taskTypes.js +37 -0
- package/dist/neurolink.d.ts +22 -1
- package/dist/neurolink.js +195 -14
- package/dist/tasks/backends/bullmqBackend.d.ts +32 -0
- package/dist/tasks/backends/bullmqBackend.js +188 -0
- package/dist/tasks/backends/nodeTimeoutBackend.d.ts +27 -0
- package/dist/tasks/backends/nodeTimeoutBackend.js +140 -0
- package/dist/tasks/backends/taskBackendRegistry.d.ts +31 -0
- package/dist/tasks/backends/taskBackendRegistry.js +65 -0
- package/dist/tasks/errors.d.ts +31 -0
- package/dist/tasks/errors.js +17 -0
- package/dist/tasks/store/fileTaskStore.d.ts +43 -0
- package/dist/tasks/store/fileTaskStore.js +178 -0
- package/dist/tasks/store/redisTaskStore.d.ts +42 -0
- package/dist/tasks/store/redisTaskStore.js +188 -0
- package/dist/tasks/taskExecutor.d.ts +21 -0
- package/dist/tasks/taskExecutor.js +165 -0
- package/dist/tasks/taskManager.d.ts +60 -0
- package/dist/tasks/taskManager.js +392 -0
- package/dist/tasks/tools/taskTools.d.ts +135 -0
- package/dist/tasks/tools/taskTools.js +273 -0
- package/dist/types/configTypes.d.ts +3 -0
- package/dist/types/generateTypes.d.ts +42 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/streamTypes.d.ts +7 -0
- package/dist/types/taskTypes.d.ts +275 -0
- package/dist/types/taskTypes.js +36 -0
- package/package.json +4 -2
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task CLI Commands for NeuroLink
|
|
3
|
+
*
|
|
4
|
+
* Implements commands for task scheduling and management:
|
|
5
|
+
* - neurolink task create — Create a scheduled task (pure store write, exits immediately)
|
|
6
|
+
* - neurolink task list — List all tasks
|
|
7
|
+
* - neurolink task get — Show task details
|
|
8
|
+
* - neurolink task run — Run a task immediately
|
|
9
|
+
* - neurolink task pause — Pause a task
|
|
10
|
+
* - neurolink task resume — Resume a paused task
|
|
11
|
+
* - neurolink task update — Update a task
|
|
12
|
+
* - neurolink task delete — Delete a task
|
|
13
|
+
* - neurolink task logs — View run history
|
|
14
|
+
* - neurolink task start — Start worker (keeps process alive for scheduled tasks)
|
|
15
|
+
* - neurolink task stop — Stop a running daemon worker
|
|
16
|
+
* - neurolink task status — Show worker status
|
|
17
|
+
*/
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
import { openSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { mkdirSync } from "node:fs";
|
|
22
|
+
import chalk from "chalk";
|
|
23
|
+
import { nanoid } from "nanoid";
|
|
24
|
+
import ora from "ora";
|
|
25
|
+
import { TASK_DEFAULTS } from "../../lib/types/taskTypes.js";
|
|
26
|
+
import { StateFileManager, isProcessRunning, formatUptime, getNeuroLinkDir, ensureStateDir, } from "../utils/serverUtils.js";
|
|
27
|
+
const workerState = new StateFileManager("task-worker-state.json");
|
|
28
|
+
/**
|
|
29
|
+
* Parse human-readable duration to milliseconds.
|
|
30
|
+
* Supports: 30s, 5m, 2h, 1d, or raw ms number.
|
|
31
|
+
*/
|
|
32
|
+
function parseDuration(input) {
|
|
33
|
+
const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
34
|
+
if (!match) {
|
|
35
|
+
const num = Number(input);
|
|
36
|
+
if (isNaN(num)) {
|
|
37
|
+
throw new Error(`Invalid duration: "${input}"`);
|
|
38
|
+
}
|
|
39
|
+
return num;
|
|
40
|
+
}
|
|
41
|
+
const value = parseFloat(match[1]);
|
|
42
|
+
const unit = (match[2] ?? "").toLowerCase();
|
|
43
|
+
switch (unit) {
|
|
44
|
+
case "s":
|
|
45
|
+
return value * 1000;
|
|
46
|
+
case "m":
|
|
47
|
+
return value * 60 * 1000;
|
|
48
|
+
case "h":
|
|
49
|
+
return value * 60 * 60 * 1000;
|
|
50
|
+
case "d":
|
|
51
|
+
return value * 24 * 60 * 60 * 1000;
|
|
52
|
+
default:
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export class TaskCommandFactory {
|
|
57
|
+
static createTaskCommands() {
|
|
58
|
+
return {
|
|
59
|
+
command: "task <subcommand>",
|
|
60
|
+
describe: "Manage scheduled and self-running tasks",
|
|
61
|
+
builder: (yargs) => {
|
|
62
|
+
return yargs
|
|
63
|
+
.command("create", "Create a scheduled task", (y) => y
|
|
64
|
+
.option("name", {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Task name",
|
|
67
|
+
demandOption: true,
|
|
68
|
+
})
|
|
69
|
+
.option("prompt", {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "Prompt to execute on each run",
|
|
72
|
+
demandOption: true,
|
|
73
|
+
})
|
|
74
|
+
.option("cron", {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: 'Cron expression (e.g. "0 9 * * *"). Mutually exclusive with --every and --at.',
|
|
77
|
+
})
|
|
78
|
+
.option("timezone", {
|
|
79
|
+
type: "string",
|
|
80
|
+
description: 'IANA timezone for cron (e.g. "America/New_York")',
|
|
81
|
+
})
|
|
82
|
+
.option("every", {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "Interval duration (e.g. 30s, 5m, 2h, 1d). Mutually exclusive with --cron and --at.",
|
|
85
|
+
})
|
|
86
|
+
.option("at", {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: "ISO 8601 timestamp for one-shot (e.g. 2026-04-01T14:00:00Z). Mutually exclusive with --cron and --every.",
|
|
89
|
+
})
|
|
90
|
+
.option("mode", {
|
|
91
|
+
type: "string",
|
|
92
|
+
choices: ["isolated", "continuation"],
|
|
93
|
+
default: "isolated",
|
|
94
|
+
description: '"isolated" = fresh context per run. "continuation" = preserves history across runs.',
|
|
95
|
+
})
|
|
96
|
+
.option("provider", {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: "AI provider override",
|
|
99
|
+
})
|
|
100
|
+
.option("model", {
|
|
101
|
+
type: "string",
|
|
102
|
+
description: "Model override",
|
|
103
|
+
})
|
|
104
|
+
.option("max-runs", {
|
|
105
|
+
type: "number",
|
|
106
|
+
description: "Maximum number of executions",
|
|
107
|
+
})
|
|
108
|
+
.option("max-tokens", {
|
|
109
|
+
type: "number",
|
|
110
|
+
description: "Max tokens per AI response",
|
|
111
|
+
})
|
|
112
|
+
.option("temperature", {
|
|
113
|
+
type: "number",
|
|
114
|
+
description: "Temperature (0-2)",
|
|
115
|
+
})
|
|
116
|
+
.option("system-prompt", {
|
|
117
|
+
type: "string",
|
|
118
|
+
description: "System prompt override",
|
|
119
|
+
})
|
|
120
|
+
.check((argv) => {
|
|
121
|
+
const scheduleFlags = [argv.cron, argv.every, argv.at].filter(Boolean);
|
|
122
|
+
if (scheduleFlags.length === 0) {
|
|
123
|
+
throw new Error("Must specify one of: --cron, --every, or --at");
|
|
124
|
+
}
|
|
125
|
+
if (scheduleFlags.length > 1) {
|
|
126
|
+
throw new Error("Only one of --cron, --every, or --at can be used");
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}), async (argv) => {
|
|
130
|
+
await TaskCommandFactory.executeCreate(argv);
|
|
131
|
+
})
|
|
132
|
+
.command("list", "List all tasks", (y) => y.option("status", {
|
|
133
|
+
type: "string",
|
|
134
|
+
choices: [
|
|
135
|
+
"active",
|
|
136
|
+
"paused",
|
|
137
|
+
"completed",
|
|
138
|
+
"failed",
|
|
139
|
+
"cancelled",
|
|
140
|
+
"pending",
|
|
141
|
+
],
|
|
142
|
+
description: "Filter by status",
|
|
143
|
+
}), async (argv) => {
|
|
144
|
+
await TaskCommandFactory.executeList(argv);
|
|
145
|
+
})
|
|
146
|
+
.command("get <task-id>", "Show details of a task", (y) => y.positional("task-id", {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "Task ID",
|
|
149
|
+
demandOption: true,
|
|
150
|
+
}), async (argv) => {
|
|
151
|
+
await TaskCommandFactory.executeGet(argv);
|
|
152
|
+
})
|
|
153
|
+
.command("run <task-id>", "Run a task immediately", (y) => y.positional("task-id", {
|
|
154
|
+
type: "string",
|
|
155
|
+
description: "Task ID",
|
|
156
|
+
demandOption: true,
|
|
157
|
+
}), async (argv) => {
|
|
158
|
+
await TaskCommandFactory.executeRun(argv);
|
|
159
|
+
})
|
|
160
|
+
.command("pause <task-id>", "Pause a scheduled task", (y) => y.positional("task-id", {
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "Task ID",
|
|
163
|
+
demandOption: true,
|
|
164
|
+
}), async (argv) => {
|
|
165
|
+
await TaskCommandFactory.executePause(argv);
|
|
166
|
+
})
|
|
167
|
+
.command("resume <task-id>", "Resume a paused task", (y) => y.positional("task-id", {
|
|
168
|
+
type: "string",
|
|
169
|
+
description: "Task ID",
|
|
170
|
+
demandOption: true,
|
|
171
|
+
}), async (argv) => {
|
|
172
|
+
await TaskCommandFactory.executeResume(argv);
|
|
173
|
+
})
|
|
174
|
+
.command("update <task-id>", "Update a task", (y) => y
|
|
175
|
+
.positional("task-id", {
|
|
176
|
+
type: "string",
|
|
177
|
+
description: "Task ID",
|
|
178
|
+
demandOption: true,
|
|
179
|
+
})
|
|
180
|
+
.option("prompt", { type: "string", description: "New prompt" })
|
|
181
|
+
.option("cron", {
|
|
182
|
+
type: "string",
|
|
183
|
+
description: "New cron expression",
|
|
184
|
+
})
|
|
185
|
+
.option("every", {
|
|
186
|
+
type: "string",
|
|
187
|
+
description: "New interval duration",
|
|
188
|
+
})
|
|
189
|
+
.option("at", {
|
|
190
|
+
type: "string",
|
|
191
|
+
description: "New one-shot timestamp",
|
|
192
|
+
})
|
|
193
|
+
.option("mode", {
|
|
194
|
+
type: "string",
|
|
195
|
+
choices: ["isolated", "continuation"],
|
|
196
|
+
description: "New execution mode",
|
|
197
|
+
}), async (argv) => {
|
|
198
|
+
await TaskCommandFactory.executeUpdate(argv);
|
|
199
|
+
})
|
|
200
|
+
.command("delete <task-id>", "Delete a task", (y) => y.positional("task-id", {
|
|
201
|
+
type: "string",
|
|
202
|
+
description: "Task ID",
|
|
203
|
+
demandOption: true,
|
|
204
|
+
}), async (argv) => {
|
|
205
|
+
await TaskCommandFactory.executeDelete(argv);
|
|
206
|
+
})
|
|
207
|
+
.command("logs <task-id>", "View run history for a task", (y) => y
|
|
208
|
+
.positional("task-id", {
|
|
209
|
+
type: "string",
|
|
210
|
+
description: "Task ID",
|
|
211
|
+
demandOption: true,
|
|
212
|
+
})
|
|
213
|
+
.option("limit", {
|
|
214
|
+
type: "number",
|
|
215
|
+
default: 20,
|
|
216
|
+
description: "Max entries to show",
|
|
217
|
+
})
|
|
218
|
+
.option("status", {
|
|
219
|
+
type: "string",
|
|
220
|
+
choices: ["success", "error"],
|
|
221
|
+
description: "Filter by run status",
|
|
222
|
+
})
|
|
223
|
+
.option("full", {
|
|
224
|
+
type: "boolean",
|
|
225
|
+
default: false,
|
|
226
|
+
description: "Show full output (no truncation)",
|
|
227
|
+
}), async (argv) => {
|
|
228
|
+
await TaskCommandFactory.executeLogs(argv);
|
|
229
|
+
})
|
|
230
|
+
.command("start", "Start task worker — keeps process alive to execute scheduled tasks", (y) => y.option("daemon", {
|
|
231
|
+
type: "boolean",
|
|
232
|
+
alias: "d",
|
|
233
|
+
default: false,
|
|
234
|
+
description: "Run worker as a background daemon (detached process)",
|
|
235
|
+
}), async (argv) => {
|
|
236
|
+
await TaskCommandFactory.executeStart(argv);
|
|
237
|
+
})
|
|
238
|
+
.command("stop", "Stop the background task worker daemon", () => { }, async () => {
|
|
239
|
+
await TaskCommandFactory.executeStop();
|
|
240
|
+
})
|
|
241
|
+
.command("status", "Show task worker status", () => { }, async () => {
|
|
242
|
+
await TaskCommandFactory.executeStatus();
|
|
243
|
+
})
|
|
244
|
+
.command({
|
|
245
|
+
// Hidden subcommand — spawned by `task start --daemon`
|
|
246
|
+
command: "_worker",
|
|
247
|
+
describe: false,
|
|
248
|
+
handler: async () => {
|
|
249
|
+
await TaskCommandFactory.executeWorkerProcess();
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
.demandCommand(1, "Please specify a task subcommand");
|
|
253
|
+
},
|
|
254
|
+
handler: () => { },
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// ── Helpers ──────────────────────────────────────────────
|
|
258
|
+
/**
|
|
259
|
+
* Get a full NeuroLink instance (with MCP, tools, providers).
|
|
260
|
+
* Used only by commands that execute AI: run, start/_worker.
|
|
261
|
+
*/
|
|
262
|
+
static async getNeuroLink() {
|
|
263
|
+
const { NeuroLink } = await import("../../lib/neurolink.js");
|
|
264
|
+
return new NeuroLink();
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get a direct TaskStore instance for pure store operations.
|
|
268
|
+
* Bypasses NeuroLink entirely — no MCP, no providers, no tools.
|
|
269
|
+
* Respects the same backend selection as TaskManager so both paths
|
|
270
|
+
* read/write the same store (Redis for bullmq, file for node-timeout).
|
|
271
|
+
*
|
|
272
|
+
* Used by all management commands: create, list, get, delete, logs, pause, resume, update.
|
|
273
|
+
*/
|
|
274
|
+
static async getStore(config) {
|
|
275
|
+
const backendName = config?.backend ?? TASK_DEFAULTS.backend;
|
|
276
|
+
if (backendName === "bullmq") {
|
|
277
|
+
const { RedisTaskStore } = await import("../../lib/tasks/store/redisTaskStore.js");
|
|
278
|
+
const store = new RedisTaskStore(config ?? {});
|
|
279
|
+
await store.initialize();
|
|
280
|
+
return store;
|
|
281
|
+
}
|
|
282
|
+
const { FileTaskStore } = await import("../../lib/tasks/store/fileTaskStore.js");
|
|
283
|
+
const store = new FileTaskStore(config ?? {});
|
|
284
|
+
await store.initialize();
|
|
285
|
+
return store;
|
|
286
|
+
}
|
|
287
|
+
/** Attach event listeners and keep the process alive for scheduled task execution */
|
|
288
|
+
static enterWorkerMode(neurolink, manager) {
|
|
289
|
+
console.info(chalk.gray(" Worker running. Tasks will auto-execute on schedule."));
|
|
290
|
+
console.info(chalk.gray(" Press Ctrl+C to stop.\n"));
|
|
291
|
+
// Log task events in real-time via the public API
|
|
292
|
+
const emitter = neurolink.getEventEmitter();
|
|
293
|
+
if (emitter) {
|
|
294
|
+
emitter.on("task:completed", (result) => {
|
|
295
|
+
const r = result;
|
|
296
|
+
const preview = r.output
|
|
297
|
+
? r.output.length > 120
|
|
298
|
+
? r.output.slice(0, 120).replace(/\n/g, " ") + "..."
|
|
299
|
+
: r.output.replace(/\n/g, " ")
|
|
300
|
+
: "(no output)";
|
|
301
|
+
console.info(` ${chalk.green("✔")} ${chalk.dim(new Date().toLocaleTimeString())} ${chalk.cyan(r.taskId)} — ${r.durationMs}ms`);
|
|
302
|
+
console.info(` ${chalk.gray(preview)}`);
|
|
303
|
+
});
|
|
304
|
+
emitter.on("task:failed", (result) => {
|
|
305
|
+
const r = result;
|
|
306
|
+
console.info(` ${chalk.red("✘")} ${chalk.dim(new Date().toLocaleTimeString())} ${chalk.cyan(r.taskId)} — ${chalk.red(r.error?.slice(0, 100) || "unknown error")}`);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
// Graceful shutdown on Ctrl+C
|
|
310
|
+
const shutdown = async () => {
|
|
311
|
+
console.info(chalk.yellow("\n Shutting down..."));
|
|
312
|
+
await manager.shutdown();
|
|
313
|
+
// Clean up worker state if we are the daemon
|
|
314
|
+
const state = workerState.load();
|
|
315
|
+
if (state && state.pid === process.pid) {
|
|
316
|
+
workerState.clear();
|
|
317
|
+
}
|
|
318
|
+
process.exit(0);
|
|
319
|
+
};
|
|
320
|
+
process.on("SIGINT", shutdown);
|
|
321
|
+
process.on("SIGTERM", shutdown);
|
|
322
|
+
}
|
|
323
|
+
// ── Command Handlers ────────────────────────────────────
|
|
324
|
+
/**
|
|
325
|
+
* Create — pure store write, no NeuroLink needed.
|
|
326
|
+
* Builds the Task object directly, saves to the task store, exits immediately.
|
|
327
|
+
*/
|
|
328
|
+
static async executeCreate(argv) {
|
|
329
|
+
const spinner = ora("Creating task...").start();
|
|
330
|
+
try {
|
|
331
|
+
// Build schedule
|
|
332
|
+
let schedule;
|
|
333
|
+
if (argv.cron) {
|
|
334
|
+
schedule = {
|
|
335
|
+
type: "cron",
|
|
336
|
+
expression: argv.cron,
|
|
337
|
+
...(argv.timezone ? { timezone: argv.timezone } : {}),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
else if (argv.every) {
|
|
341
|
+
schedule = { type: "interval", every: parseDuration(argv.every) };
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
schedule = { type: "once", at: argv.at };
|
|
345
|
+
}
|
|
346
|
+
const now = new Date().toISOString();
|
|
347
|
+
const mode = argv.mode ?? TASK_DEFAULTS.mode;
|
|
348
|
+
const task = {
|
|
349
|
+
id: `task_${nanoid(12)}`,
|
|
350
|
+
name: argv.name,
|
|
351
|
+
prompt: argv.prompt,
|
|
352
|
+
schedule,
|
|
353
|
+
mode,
|
|
354
|
+
status: "active",
|
|
355
|
+
tools: TASK_DEFAULTS.tools,
|
|
356
|
+
timeout: TASK_DEFAULTS.timeout,
|
|
357
|
+
retry: {
|
|
358
|
+
maxAttempts: TASK_DEFAULTS.retry.maxAttempts,
|
|
359
|
+
backoffMs: [...TASK_DEFAULTS.retry.backoffMs],
|
|
360
|
+
},
|
|
361
|
+
runCount: 0,
|
|
362
|
+
createdAt: now,
|
|
363
|
+
updatedAt: now,
|
|
364
|
+
...(mode === "continuation"
|
|
365
|
+
? { sessionId: `session_${nanoid(12)}` }
|
|
366
|
+
: {}),
|
|
367
|
+
...(argv.provider ? { provider: argv.provider } : {}),
|
|
368
|
+
...(argv.model ? { model: argv.model } : {}),
|
|
369
|
+
...(argv.maxRuns ? { maxRuns: argv.maxRuns } : {}),
|
|
370
|
+
...(argv.maxTokens ? { maxTokens: argv.maxTokens } : {}),
|
|
371
|
+
...(argv.temperature !== undefined
|
|
372
|
+
? { temperature: argv.temperature }
|
|
373
|
+
: {}),
|
|
374
|
+
...(argv.systemPrompt ? { systemPrompt: argv.systemPrompt } : {}),
|
|
375
|
+
};
|
|
376
|
+
// Write directly to store — no NeuroLink, no MCP, no providers
|
|
377
|
+
const store = await TaskCommandFactory.getStore();
|
|
378
|
+
await store.save(task);
|
|
379
|
+
await store.shutdown();
|
|
380
|
+
spinner.succeed(chalk.green("Task created"));
|
|
381
|
+
console.info();
|
|
382
|
+
console.info(` ${chalk.bold("ID:")} ${task.id}`);
|
|
383
|
+
console.info(` ${chalk.bold("Name:")} ${task.name}`);
|
|
384
|
+
console.info(` ${chalk.bold("Status:")} ${task.status}`);
|
|
385
|
+
console.info(` ${chalk.bold("Mode:")} ${task.mode}`);
|
|
386
|
+
console.info(` ${chalk.bold("Schedule:")} ${formatSchedule(task.schedule)}`);
|
|
387
|
+
console.info();
|
|
388
|
+
console.info(chalk.dim(" Run `neurolink task start` to start the worker and execute scheduled tasks."));
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
spinner.fail(chalk.red("Failed to create task"));
|
|
392
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
static async executeList(argv) {
|
|
397
|
+
const spinner = ora("Loading tasks...").start();
|
|
398
|
+
try {
|
|
399
|
+
const store = await TaskCommandFactory.getStore();
|
|
400
|
+
const tasks = await store.list(argv.status ? { status: argv.status } : undefined);
|
|
401
|
+
spinner.stop();
|
|
402
|
+
if (tasks.length === 0) {
|
|
403
|
+
console.info(chalk.dim("No tasks found."));
|
|
404
|
+
await store.shutdown();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// Show worker status inline
|
|
408
|
+
const state = workerState.load();
|
|
409
|
+
const workerRunning = state ? isProcessRunning(state.pid) : false;
|
|
410
|
+
console.info(chalk.bold(`\nTasks (${tasks.length}) ${workerRunning ? chalk.green("● worker running") : chalk.dim("○ worker stopped")}:\n`));
|
|
411
|
+
for (const task of tasks) {
|
|
412
|
+
const statusColor = task.status === "active"
|
|
413
|
+
? chalk.green
|
|
414
|
+
: task.status === "paused"
|
|
415
|
+
? chalk.yellow
|
|
416
|
+
: task.status === "failed"
|
|
417
|
+
? chalk.red
|
|
418
|
+
: chalk.dim;
|
|
419
|
+
console.info(` ${chalk.bold(task.name)} ${chalk.dim(`(${task.id})`)}`);
|
|
420
|
+
console.info(` Status: ${statusColor(task.status)} | Mode: ${task.mode} | Runs: ${task.runCount} | Schedule: ${formatSchedule(task.schedule)}`);
|
|
421
|
+
if (task.lastRunAt) {
|
|
422
|
+
console.info(` Last run: ${chalk.dim(task.lastRunAt)}`);
|
|
423
|
+
}
|
|
424
|
+
console.info();
|
|
425
|
+
}
|
|
426
|
+
await store.shutdown();
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
spinner.fail(chalk.red("Failed to list tasks"));
|
|
430
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
static async executeGet(argv) {
|
|
435
|
+
try {
|
|
436
|
+
const store = await TaskCommandFactory.getStore();
|
|
437
|
+
const task = await store.get(argv.taskId);
|
|
438
|
+
if (!task) {
|
|
439
|
+
console.info(chalk.red(`Task not found: ${argv.taskId}`));
|
|
440
|
+
await store.shutdown();
|
|
441
|
+
process.exit(1);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
console.info();
|
|
445
|
+
console.info(` ${chalk.bold("ID:")} ${task.id}`);
|
|
446
|
+
console.info(` ${chalk.bold("Name:")} ${task.name}`);
|
|
447
|
+
console.info(` ${chalk.bold("Status:")} ${task.status}`);
|
|
448
|
+
console.info(` ${chalk.bold("Mode:")} ${task.mode}`);
|
|
449
|
+
console.info(` ${chalk.bold("Schedule:")} ${formatSchedule(task.schedule)}`);
|
|
450
|
+
console.info(` ${chalk.bold("Run count:")} ${task.runCount}`);
|
|
451
|
+
if (task.maxRuns) {
|
|
452
|
+
console.info(` ${chalk.bold("Max runs:")} ${task.maxRuns}`);
|
|
453
|
+
}
|
|
454
|
+
if (task.provider) {
|
|
455
|
+
console.info(` ${chalk.bold("Provider:")} ${task.provider}`);
|
|
456
|
+
}
|
|
457
|
+
if (task.model) {
|
|
458
|
+
console.info(` ${chalk.bold("Model:")} ${task.model}`);
|
|
459
|
+
}
|
|
460
|
+
if (task.lastRunAt) {
|
|
461
|
+
console.info(` ${chalk.bold("Last run:")} ${task.lastRunAt}`);
|
|
462
|
+
}
|
|
463
|
+
console.info(` ${chalk.bold("Created:")} ${task.createdAt}`);
|
|
464
|
+
console.info(` ${chalk.bold("Updated:")} ${task.updatedAt}`);
|
|
465
|
+
console.info(` ${chalk.bold("Prompt:")} ${task.prompt.slice(0, 200)}${task.prompt.length > 200 ? "..." : ""}`);
|
|
466
|
+
console.info();
|
|
467
|
+
await store.shutdown();
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
static async executeRun(argv) {
|
|
475
|
+
const spinner = ora("Running task...").start();
|
|
476
|
+
try {
|
|
477
|
+
const neurolink = await TaskCommandFactory.getNeuroLink();
|
|
478
|
+
const manager = neurolink.tasks;
|
|
479
|
+
const result = await manager.run(argv.taskId);
|
|
480
|
+
if (result.status === "success") {
|
|
481
|
+
spinner.succeed(chalk.green(`Task completed in ${result.durationMs}ms`));
|
|
482
|
+
if (result.output) {
|
|
483
|
+
console.info(`\n${result.output}\n`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
spinner.fail(chalk.red(`Task failed: ${result.error}`));
|
|
488
|
+
}
|
|
489
|
+
await manager.shutdown();
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
spinner.fail(chalk.red("Failed to run task"));
|
|
493
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
static async executePause(argv) {
|
|
498
|
+
try {
|
|
499
|
+
const store = await TaskCommandFactory.getStore();
|
|
500
|
+
const task = await store.get(argv.taskId);
|
|
501
|
+
if (!task) {
|
|
502
|
+
console.error(chalk.red(`Task not found: ${argv.taskId}`));
|
|
503
|
+
await store.shutdown();
|
|
504
|
+
process.exit(1);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (task.status !== "active") {
|
|
508
|
+
console.error(chalk.red(`Cannot pause task with status: ${task.status}`));
|
|
509
|
+
await store.shutdown();
|
|
510
|
+
process.exit(1);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const updated = await store.update(argv.taskId, { status: "paused" });
|
|
514
|
+
console.info(chalk.yellow(`Task "${updated.name}" paused.`));
|
|
515
|
+
await store.shutdown();
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
static async executeResume(argv) {
|
|
523
|
+
try {
|
|
524
|
+
const store = await TaskCommandFactory.getStore();
|
|
525
|
+
const task = await store.get(argv.taskId);
|
|
526
|
+
if (!task) {
|
|
527
|
+
console.error(chalk.red(`Task not found: ${argv.taskId}`));
|
|
528
|
+
await store.shutdown();
|
|
529
|
+
process.exit(1);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (task.status !== "paused") {
|
|
533
|
+
console.error(chalk.red(`Cannot resume task with status: ${task.status}`));
|
|
534
|
+
await store.shutdown();
|
|
535
|
+
process.exit(1);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const updated = await store.update(argv.taskId, { status: "active" });
|
|
539
|
+
console.info(chalk.green(`Task "${updated.name}" resumed.`));
|
|
540
|
+
await store.shutdown();
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
static async executeUpdate(argv) {
|
|
548
|
+
try {
|
|
549
|
+
const store = await TaskCommandFactory.getStore();
|
|
550
|
+
// Validate mutual exclusivity of schedule flags
|
|
551
|
+
const scheduleFlags = [argv.cron, argv.every, argv.at].filter(Boolean);
|
|
552
|
+
if (scheduleFlags.length > 1) {
|
|
553
|
+
console.error(chalk.red("Only one of --cron, --every, or --at can be used"));
|
|
554
|
+
await store.shutdown();
|
|
555
|
+
process.exit(1);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const updates = {};
|
|
559
|
+
if (argv.prompt) {
|
|
560
|
+
updates.prompt = argv.prompt;
|
|
561
|
+
}
|
|
562
|
+
if (argv.mode) {
|
|
563
|
+
updates.mode = argv.mode;
|
|
564
|
+
}
|
|
565
|
+
// Build schedule if any schedule flag is provided
|
|
566
|
+
if (argv.cron) {
|
|
567
|
+
updates.schedule = { type: "cron", expression: argv.cron };
|
|
568
|
+
}
|
|
569
|
+
else if (argv.every) {
|
|
570
|
+
updates.schedule = {
|
|
571
|
+
type: "interval",
|
|
572
|
+
every: parseDuration(argv.every),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
else if (argv.at) {
|
|
576
|
+
updates.schedule = { type: "once", at: argv.at };
|
|
577
|
+
}
|
|
578
|
+
if (Object.keys(updates).length === 0) {
|
|
579
|
+
console.info(chalk.yellow("No updates specified."));
|
|
580
|
+
await store.shutdown();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const task = await store.update(argv.taskId, updates);
|
|
584
|
+
console.info(chalk.green(`Task "${task.name}" updated.`));
|
|
585
|
+
await store.shutdown();
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
static async executeDelete(argv) {
|
|
593
|
+
try {
|
|
594
|
+
const store = await TaskCommandFactory.getStore();
|
|
595
|
+
await store.delete(argv.taskId);
|
|
596
|
+
console.info(chalk.green(`Task ${argv.taskId} deleted.`));
|
|
597
|
+
await store.shutdown();
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
static async executeLogs(argv) {
|
|
605
|
+
try {
|
|
606
|
+
const store = await TaskCommandFactory.getStore();
|
|
607
|
+
const runs = await store.getRuns(argv.taskId, {
|
|
608
|
+
limit: argv.limit,
|
|
609
|
+
status: argv.status,
|
|
610
|
+
});
|
|
611
|
+
if (runs.length === 0) {
|
|
612
|
+
console.info(chalk.dim("No runs found."));
|
|
613
|
+
await store.shutdown();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
console.info(chalk.bold(`\nRun history (${runs.length}):\n`));
|
|
617
|
+
for (const run of runs) {
|
|
618
|
+
const statusIcon = run.status === "success" ? chalk.green("✓") : chalk.red("✗");
|
|
619
|
+
const duration = `${run.durationMs}ms`;
|
|
620
|
+
console.info(` ${statusIcon} ${chalk.dim(run.runId)} ${chalk.dim(run.timestamp)} ${duration}`);
|
|
621
|
+
if (run.error) {
|
|
622
|
+
console.info(` ${chalk.red(run.error)}`);
|
|
623
|
+
}
|
|
624
|
+
if (run.output) {
|
|
625
|
+
if (argv.full) {
|
|
626
|
+
console.info(` ${run.output}`);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
const preview = run.output.length > 120
|
|
630
|
+
? run.output.slice(0, 120) + "..."
|
|
631
|
+
: run.output;
|
|
632
|
+
console.info(` ${chalk.dim(preview)}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
console.info();
|
|
637
|
+
await store.shutdown();
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
// ── Start / Stop / Status ──────────────────────────────
|
|
645
|
+
static async executeStart(argv) {
|
|
646
|
+
// Check if daemon is already running
|
|
647
|
+
const existing = workerState.load();
|
|
648
|
+
if (existing && isProcessRunning(existing.pid)) {
|
|
649
|
+
console.info(chalk.yellow(`Worker already running (PID ${existing.pid}, started ${existing.startedAt}).`));
|
|
650
|
+
console.info(chalk.dim(" Run `neurolink task stop` to stop it first."));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (argv.daemon) {
|
|
654
|
+
// Spawn detached worker process
|
|
655
|
+
await TaskCommandFactory.spawnDaemon();
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
// Foreground worker
|
|
659
|
+
await TaskCommandFactory.runForegroundWorker();
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
static async executeStop() {
|
|
663
|
+
const state = workerState.load();
|
|
664
|
+
if (!state) {
|
|
665
|
+
console.info(chalk.dim("No worker daemon is running."));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (!isProcessRunning(state.pid)) {
|
|
669
|
+
console.info(chalk.dim("Worker daemon is not running (stale state)."));
|
|
670
|
+
workerState.clear();
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
process.kill(state.pid, "SIGTERM");
|
|
675
|
+
console.info(chalk.green(`Worker daemon stopped (PID ${state.pid}).`));
|
|
676
|
+
console.info(chalk.dim(` Logs: ${state.logFile}`));
|
|
677
|
+
workerState.clear();
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
console.error(chalk.red(`Failed to stop worker: ${error instanceof Error ? error.message : String(error)}`));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
static async executeStatus() {
|
|
684
|
+
const state = workerState.load();
|
|
685
|
+
if (!state) {
|
|
686
|
+
console.info(chalk.dim("No worker daemon registered."));
|
|
687
|
+
console.info(chalk.dim(" Run `neurolink task start --daemon` to start one."));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const running = isProcessRunning(state.pid);
|
|
691
|
+
console.info();
|
|
692
|
+
console.info(` ${chalk.bold("Status:")} ${running ? chalk.green("● running") : chalk.red("✘ stopped")}`);
|
|
693
|
+
console.info(` ${chalk.bold("PID:")} ${state.pid}`);
|
|
694
|
+
console.info(` ${chalk.bold("Started:")} ${state.startedAt}`);
|
|
695
|
+
if (running) {
|
|
696
|
+
const uptimeMs = Date.now() - new Date(state.startedAt).getTime();
|
|
697
|
+
console.info(` ${chalk.bold("Uptime:")} ${formatUptime(uptimeMs)}`);
|
|
698
|
+
}
|
|
699
|
+
console.info(` ${chalk.bold("Logs:")} ${state.logFile}`);
|
|
700
|
+
console.info();
|
|
701
|
+
if (!running) {
|
|
702
|
+
workerState.clear();
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
// ── Daemon Spawn ───────────────────────────────────────
|
|
706
|
+
static async spawnDaemon() {
|
|
707
|
+
const entryScript = process.argv[1];
|
|
708
|
+
if (!entryScript) {
|
|
709
|
+
console.error(chalk.red("Cannot determine CLI entry point."));
|
|
710
|
+
process.exit(1);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
// Set up log file
|
|
714
|
+
ensureStateDir();
|
|
715
|
+
const logsDir = join(getNeuroLinkDir(), "logs");
|
|
716
|
+
mkdirSync(logsDir, { recursive: true });
|
|
717
|
+
const logFile = join(logsDir, "task-worker.log");
|
|
718
|
+
const logFd = openSync(logFile, "a");
|
|
719
|
+
const args = [entryScript, "task", "_worker"];
|
|
720
|
+
const child = spawn(process.execPath, args, {
|
|
721
|
+
detached: true,
|
|
722
|
+
stdio: ["ignore", logFd, logFd],
|
|
723
|
+
cwd: process.cwd(),
|
|
724
|
+
env: { ...process.env },
|
|
725
|
+
});
|
|
726
|
+
child.unref();
|
|
727
|
+
const pid = child.pid;
|
|
728
|
+
if (!pid) {
|
|
729
|
+
console.error(chalk.red("Failed to spawn worker daemon."));
|
|
730
|
+
process.exit(1);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
workerState.save({
|
|
734
|
+
pid,
|
|
735
|
+
startedAt: new Date().toISOString(),
|
|
736
|
+
logFile,
|
|
737
|
+
});
|
|
738
|
+
console.info(chalk.green(`Worker daemon started (PID ${pid}).`));
|
|
739
|
+
console.info(chalk.dim(` Logs: ${logFile}`));
|
|
740
|
+
console.info(chalk.dim(" Run `neurolink task stop` to stop it."));
|
|
741
|
+
console.info(chalk.dim(" Run `neurolink task status` to check on it."));
|
|
742
|
+
}
|
|
743
|
+
// ── Foreground Worker ──────────────────────────────────
|
|
744
|
+
static async runForegroundWorker() {
|
|
745
|
+
const neurolink = await TaskCommandFactory.getNeuroLink();
|
|
746
|
+
const manager = neurolink.tasks;
|
|
747
|
+
// Trigger initialization and list active tasks
|
|
748
|
+
const tasks = await manager.list({ status: "active" });
|
|
749
|
+
console.info(chalk.bold("\n NeuroLink Task Worker"));
|
|
750
|
+
console.info(chalk.gray(" ─────────────────────────────────"));
|
|
751
|
+
console.info(` Active tasks: ${chalk.cyan(String(tasks.length))}`);
|
|
752
|
+
for (const t of tasks) {
|
|
753
|
+
console.info(` ${chalk.gray("•")} ${t.name} (${chalk.dim(t.id)}) — ${formatSchedule(t.schedule)}`);
|
|
754
|
+
}
|
|
755
|
+
if (tasks.length === 0) {
|
|
756
|
+
console.info(chalk.yellow("\n No active tasks. Create one first with: neurolink task create"));
|
|
757
|
+
await manager.shutdown();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
console.info();
|
|
761
|
+
TaskCommandFactory.enterWorkerMode(neurolink, manager);
|
|
762
|
+
await new Promise(() => { }); // Block forever until Ctrl+C
|
|
763
|
+
}
|
|
764
|
+
// ── Hidden _worker process (spawned by --daemon) ───────
|
|
765
|
+
static async executeWorkerProcess() {
|
|
766
|
+
// This runs in the detached child process
|
|
767
|
+
const neurolink = await TaskCommandFactory.getNeuroLink();
|
|
768
|
+
const manager = neurolink.tasks;
|
|
769
|
+
const tasks = await manager.list({ status: "active" });
|
|
770
|
+
console.info(`[task-worker] Started at ${new Date().toISOString()}, active tasks: ${tasks.length}`);
|
|
771
|
+
for (const t of tasks) {
|
|
772
|
+
console.info(`[task-worker] ${t.name} (${t.id}) — ${formatSchedule(t.schedule)}`);
|
|
773
|
+
}
|
|
774
|
+
if (tasks.length === 0) {
|
|
775
|
+
console.info("[task-worker] No active tasks, exiting.");
|
|
776
|
+
workerState.clear();
|
|
777
|
+
await manager.shutdown();
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
// Listen for task events (logged to file since stdio goes to log)
|
|
781
|
+
const emitter = neurolink.getEventEmitter();
|
|
782
|
+
if (emitter) {
|
|
783
|
+
emitter.on("task:completed", (result) => {
|
|
784
|
+
const r = result;
|
|
785
|
+
const preview = r.output
|
|
786
|
+
? r.output.length > 200
|
|
787
|
+
? r.output.slice(0, 200) + "..."
|
|
788
|
+
: r.output
|
|
789
|
+
: "(no output)";
|
|
790
|
+
console.info(`[task-worker] ✔ ${new Date().toISOString()} ${r.taskId} — ${r.durationMs}ms — ${preview}`);
|
|
791
|
+
});
|
|
792
|
+
emitter.on("task:failed", (result) => {
|
|
793
|
+
const r = result;
|
|
794
|
+
console.error(`[task-worker] ✘ ${new Date().toISOString()} ${r.taskId} — ${r.error?.slice(0, 200) || "unknown error"}`);
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
// Graceful shutdown
|
|
798
|
+
const shutdown = async () => {
|
|
799
|
+
console.info(`[task-worker] Shutting down at ${new Date().toISOString()}`);
|
|
800
|
+
await manager.shutdown();
|
|
801
|
+
workerState.clear();
|
|
802
|
+
process.exit(0);
|
|
803
|
+
};
|
|
804
|
+
process.on("SIGINT", shutdown);
|
|
805
|
+
process.on("SIGTERM", shutdown);
|
|
806
|
+
// Block forever
|
|
807
|
+
await new Promise(() => { });
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
// ── Helpers ─────────────────────────────────────────────
|
|
811
|
+
function formatSchedule(schedule) {
|
|
812
|
+
if (schedule.type === "cron") {
|
|
813
|
+
return `cron "${schedule.expression}"${schedule.timezone ? ` (${schedule.timezone})` : ""}`;
|
|
814
|
+
}
|
|
815
|
+
if (schedule.type === "interval") {
|
|
816
|
+
return `every ${formatDuration(schedule.every)}`;
|
|
817
|
+
}
|
|
818
|
+
return `once at ${typeof schedule.at === "string" ? schedule.at : schedule.at.toISOString()}`;
|
|
819
|
+
}
|
|
820
|
+
function formatDuration(ms) {
|
|
821
|
+
if (ms < 1000) {
|
|
822
|
+
return `${ms}ms`;
|
|
823
|
+
}
|
|
824
|
+
if (ms < 60_000) {
|
|
825
|
+
return `${ms / 1000}s`;
|
|
826
|
+
}
|
|
827
|
+
if (ms < 3_600_000) {
|
|
828
|
+
return `${ms / 60_000}m`;
|
|
829
|
+
}
|
|
830
|
+
if (ms < 86_400_000) {
|
|
831
|
+
return `${ms / 3_600_000}h`;
|
|
832
|
+
}
|
|
833
|
+
return `${ms / 86_400_000}d`;
|
|
834
|
+
}
|
|
835
|
+
//# sourceMappingURL=task.js.map
|