@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
868 lines • 38.7 kB
JavaScript
/**
* Task CLI Commands for NeuroLink
*
* Implements commands for task scheduling and management:
* - neurolink task create — Create a scheduled task (pure store write, exits immediately)
* - neurolink task list — List all tasks
* - neurolink task get — Show task details
* - neurolink task run — Run a task immediately
* - neurolink task pause — Pause a task
* - neurolink task resume — Resume a paused task
* - neurolink task update — Update a task
* - neurolink task delete — Delete a task
* - neurolink task logs — View run history
* - neurolink task start — Start worker (keeps process alive for scheduled tasks)
* - neurolink task stop — Stop a running daemon worker
* - neurolink task status — Show worker status
*/
import { spawn } from "node:child_process";
import { mkdirSync, openSync } from "node:fs";
import { join } from "node:path";
import chalk from "chalk";
import { nanoid } from "nanoid";
import ora from "ora";
import { TASK_DEFAULTS } from "../../lib/types/index.js";
import { ensureStateDir, formatUptime, getNeuroLinkDir, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
const workerState = new StateFileManager("task-worker-state.json");
/**
* Parse human-readable duration to milliseconds.
* Supports: 30s, 5m, 2h, 1d, or raw ms number.
*/
function parseDuration(input) {
const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
if (!match) {
const num = Number(input);
if (isNaN(num)) {
throw new Error(`Invalid duration: "${input}"`);
}
return num;
}
const value = parseFloat(match[1]);
const unit = (match[2] ?? "").toLowerCase();
switch (unit) {
case "s":
return value * 1000;
case "m":
return value * 60 * 1000;
case "h":
return value * 60 * 60 * 1000;
case "d":
return value * 24 * 60 * 60 * 1000;
default:
return value;
}
}
export class TaskCommandFactory {
static createTaskCommands() {
return {
command: "task <subcommand>",
describe: "Manage scheduled and self-running tasks",
builder: (yargs) => {
return yargs
.command("create", "Create a scheduled task", (y) => y
.option("name", {
type: "string",
description: "Task name",
demandOption: true,
})
.option("prompt", {
type: "string",
description: "Prompt to execute on each run",
demandOption: true,
})
.option("cron", {
type: "string",
description: 'Cron expression (e.g. "0 9 * * *"). Mutually exclusive with --every and --at.',
})
.option("timezone", {
type: "string",
description: 'IANA timezone for cron (e.g. "America/New_York")',
})
.option("every", {
type: "string",
description: "Interval duration (e.g. 30s, 5m, 2h, 1d). Mutually exclusive with --cron and --at.",
})
.option("at", {
type: "string",
description: "ISO 8601 timestamp for one-shot (e.g. 2026-04-01T14:00:00Z). Mutually exclusive with --cron and --every.",
})
.option("mode", {
type: "string",
choices: ["isolated", "continuation"],
default: "isolated",
description: '"isolated" = fresh context per run. "continuation" = preserves history across runs.',
})
.option("provider", {
type: "string",
description: "AI provider override",
})
.option("model", {
type: "string",
description: "Model override",
})
.option("max-runs", {
type: "number",
description: "Maximum number of executions",
})
.option("max-tokens", {
type: "number",
description: "Max tokens per AI response",
})
.option("temperature", {
type: "number",
description: "Temperature (0-2)",
})
.option("system-prompt", {
type: "string",
description: "System prompt override",
})
.check((argv) => {
const scheduleFlags = [argv.cron, argv.every, argv.at].filter(Boolean);
if (scheduleFlags.length === 0) {
throw new Error("Must specify one of: --cron, --every, or --at");
}
if (scheduleFlags.length > 1) {
throw new Error("Only one of --cron, --every, or --at can be used");
}
return true;
}), async (argv) => {
await TaskCommandFactory.executeCreate(argv);
})
.command("list", "List all tasks", (y) => y.option("status", {
type: "string",
choices: [
"active",
"paused",
"completed",
"failed",
"cancelled",
"pending",
],
description: "Filter by status",
}), async (argv) => {
await TaskCommandFactory.executeList(argv);
})
.command("get <task-id>", "Show details of a task", (y) => y.positional("task-id", {
type: "string",
description: "Task ID",
demandOption: true,
}), async (argv) => {
await TaskCommandFactory.executeGet(argv);
})
.command("run <task-id>", "Run a task immediately", (y) => y.positional("task-id", {
type: "string",
description: "Task ID",
demandOption: true,
}), async (argv) => {
await TaskCommandFactory.executeRun(argv);
})
.command("pause <task-id>", "Pause a scheduled task", (y) => y.positional("task-id", {
type: "string",
description: "Task ID",
demandOption: true,
}), async (argv) => {
await TaskCommandFactory.executePause(argv);
})
.command("resume <task-id>", "Resume a paused task", (y) => y.positional("task-id", {
type: "string",
description: "Task ID",
demandOption: true,
}), async (argv) => {
await TaskCommandFactory.executeResume(argv);
})
.command("update <task-id>", "Update a task", (y) => y
.positional("task-id", {
type: "string",
description: "Task ID",
demandOption: true,
})
.option("prompt", { type: "string", description: "New prompt" })
.option("cron", {
type: "string",
description: "New cron expression",
})
.option("every", {
type: "string",
description: "New interval duration",
})
.option("at", {
type: "string",
description: "New one-shot timestamp",
})
.option("mode", {
type: "string",
choices: ["isolated", "continuation"],
description: "New execution mode",
}), async (argv) => {
await TaskCommandFactory.executeUpdate(argv);
})
.command("delete <task-id>", "Delete a task", (y) => y.positional("task-id", {
type: "string",
description: "Task ID",
demandOption: true,
}), async (argv) => {
await TaskCommandFactory.executeDelete(argv);
})
.command("logs <task-id>", "View run history for a task", (y) => y
.positional("task-id", {
type: "string",
description: "Task ID",
demandOption: true,
})
.option("limit", {
type: "number",
default: 20,
description: "Max entries to show",
})
.option("status", {
type: "string",
choices: ["success", "error"],
description: "Filter by run status",
})
.option("full", {
type: "boolean",
default: false,
description: "Show full output (no truncation)",
}), async (argv) => {
await TaskCommandFactory.executeLogs(argv);
})
.command("start", "Start task worker — keeps process alive to execute scheduled tasks", (y) => y.option("daemon", {
type: "boolean",
alias: "d",
default: false,
description: "Run worker as a background daemon (detached process)",
}), async (argv) => {
await TaskCommandFactory.executeStart(argv);
})
.command("stop", "Stop the background task worker daemon", () => { }, async () => {
await TaskCommandFactory.executeStop();
})
.command("status", "Show task worker status", () => { }, async () => {
await TaskCommandFactory.executeStatus();
})
.command({
// Hidden subcommand — spawned by `task start --daemon`
command: "_worker",
describe: false,
handler: async () => {
await TaskCommandFactory.executeWorkerProcess();
},
})
.demandCommand(1, "Please specify a task subcommand");
},
handler: () => { },
};
}
// ── Helpers ──────────────────────────────────────────────
/**
* Get a full NeuroLink instance (with MCP, tools, providers).
* Used only by commands that execute AI: run, start/_worker.
*/
static async getNeuroLink() {
const { NeuroLink } = await import("../../lib/neurolink.js");
return new NeuroLink();
}
/**
* Get a direct TaskStore instance for pure store operations.
* Bypasses NeuroLink entirely — no MCP, no providers, no tools.
* Respects the same backend selection as TaskManager so both paths
* read/write the same store (Redis for bullmq, file for node-timeout).
*
* Used by all management commands: create, list, get, delete, logs, pause, resume, update.
*/
static async getStore(config) {
const backendName = config?.backend ?? TASK_DEFAULTS.backend;
if (backendName === "bullmq") {
const { RedisTaskStore } = await import("../../lib/tasks/store/redisTaskStore.js");
const store = new RedisTaskStore(config ?? {});
await store.initialize();
return store;
}
const { FileTaskStore } = await import("../../lib/tasks/store/fileTaskStore.js");
const store = new FileTaskStore(config ?? {});
await store.initialize();
return store;
}
/** Attach autoresearch-specific event listeners for worker log output */
static attachAutoresearchEventListeners(emitter) {
emitter.on("autoresearch:phase-changed", (event) => {
const e = event;
console.info(` ${chalk.magenta("⟳")} ${chalk.dim(new Date().toLocaleTimeString())} ${chalk.magenta("phase")} ${e.from} → ${chalk.bold(e.to ?? "?")}${e.tag ? ` [${e.tag}]` : ""}`);
});
emitter.on("autoresearch:experiment-completed", (event) => {
const e = event;
const icon = e.status === "keep"
? chalk.green("✔")
: e.status === "discard"
? chalk.yellow("↩")
: chalk.red("✘");
console.info(` ${icon} ${chalk.dim(new Date().toLocaleTimeString())} ${chalk.cyan("experiment")} ${e.status} metric=${e.metric ?? "N/A"} commit=${(e.commit ?? "").slice(0, 7)} ${e.durationMs ? `${e.durationMs}ms` : ""}`);
});
emitter.on("autoresearch:metric-improved", (event) => {
const e = event;
console.info(` ${chalk.green("★")} ${chalk.dim(new Date().toLocaleTimeString())} ${chalk.green("new best metric")} ${e.previousBest} → ${chalk.bold(String(e.newBest))} (${e.direction})`);
});
emitter.on("autoresearch:error", (event) => {
const e = event;
const errMsg = e.error ?? e.message ?? "unknown";
console.info(` ${chalk.red("⚠")} ${chalk.dim(new Date().toLocaleTimeString())} ${chalk.red("autoresearch error")} [${e.code}] ${errMsg.slice(0, 100)}${e.phase ? ` (phase: ${e.phase})` : ""}`);
});
}
/** Attach event listeners and keep the process alive for scheduled task execution */
static enterWorkerMode(neurolink, manager) {
console.info(chalk.gray(" Worker running. Tasks will auto-execute on schedule."));
console.info(chalk.gray(" Press Ctrl+C to stop.\n"));
// Log task events in real-time via the public API
const emitter = neurolink.getEventEmitter();
if (emitter) {
emitter.on("task:completed", (result) => {
const r = result;
const preview = r.output
? r.output.length > 120
? r.output.slice(0, 120).replace(/\n/g, " ") + "..."
: r.output.replace(/\n/g, " ")
: "(no output)";
console.info(` ${chalk.green("✔")} ${chalk.dim(new Date().toLocaleTimeString())} ${chalk.cyan(r.taskId)} — ${r.durationMs}ms`);
console.info(` ${chalk.gray(preview)}`);
});
emitter.on("task:failed", (result) => {
const r = result;
console.info(` ${chalk.red("✘")} ${chalk.dim(new Date().toLocaleTimeString())} ${chalk.cyan(r.taskId)} — ${chalk.red(r.error?.slice(0, 100) || "unknown error")}`);
});
// Autoresearch lifecycle events
TaskCommandFactory.attachAutoresearchEventListeners(emitter);
}
// Graceful shutdown on Ctrl+C
const shutdown = async () => {
console.info(chalk.yellow("\n Shutting down..."));
await manager.shutdown();
// Clean up worker state if we are the daemon
const state = workerState.load();
if (state && state.pid === process.pid) {
workerState.clear();
}
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}
// ── Command Handlers ────────────────────────────────────
/**
* Create — pure store write, no NeuroLink needed.
* Builds the Task object directly, saves to the task store, exits immediately.
*/
static async executeCreate(argv) {
const spinner = ora("Creating task...").start();
try {
// Build schedule
let schedule;
if (argv.cron) {
schedule = {
type: "cron",
expression: argv.cron,
...(argv.timezone ? { timezone: argv.timezone } : {}),
};
}
else if (argv.every) {
schedule = { type: "interval", every: parseDuration(argv.every) };
}
else {
if (!argv.at) {
throw new Error("One-time tasks require --at");
}
schedule = { type: "once", at: argv.at };
}
const now = new Date().toISOString();
const mode = argv.mode ?? TASK_DEFAULTS.mode;
const task = {
id: `task_${nanoid(12)}`,
name: argv.name,
prompt: argv.prompt,
schedule,
mode,
type: "standard",
status: "active",
tools: TASK_DEFAULTS.tools,
timeout: TASK_DEFAULTS.timeout,
retry: {
maxAttempts: TASK_DEFAULTS.retry.maxAttempts,
backoffMs: [...TASK_DEFAULTS.retry.backoffMs],
},
runCount: 0,
createdAt: now,
updatedAt: now,
...(mode === "continuation"
? { sessionId: `session_${nanoid(12)}` }
: {}),
...(argv.provider ? { provider: argv.provider } : {}),
...(argv.model ? { model: argv.model } : {}),
...(argv.maxRuns ? { maxRuns: argv.maxRuns } : {}),
...(argv.maxTokens ? { maxTokens: argv.maxTokens } : {}),
...(argv.temperature !== undefined
? { temperature: argv.temperature }
: {}),
...(argv.systemPrompt ? { systemPrompt: argv.systemPrompt } : {}),
};
// Write directly to store — no NeuroLink, no MCP, no providers
const store = await TaskCommandFactory.getStore();
await store.save(task);
await store.shutdown();
spinner.succeed(chalk.green("Task created"));
console.info();
console.info(` ${chalk.bold("ID:")} ${task.id}`);
console.info(` ${chalk.bold("Name:")} ${task.name}`);
console.info(` ${chalk.bold("Status:")} ${task.status}`);
console.info(` ${chalk.bold("Mode:")} ${task.mode}`);
console.info(` ${chalk.bold("Schedule:")} ${formatSchedule(task.schedule)}`);
console.info();
console.info(chalk.dim(" Run `neurolink task start` to start the worker and execute scheduled tasks."));
}
catch (error) {
spinner.fail(chalk.red("Failed to create task"));
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
static async executeList(argv) {
const spinner = ora("Loading tasks...").start();
try {
const store = await TaskCommandFactory.getStore();
const tasks = await store.list(argv.status ? { status: argv.status } : undefined);
spinner.stop();
if (tasks.length === 0) {
console.info(chalk.dim("No tasks found."));
await store.shutdown();
return;
}
// Show worker status inline
const state = workerState.load();
const workerRunning = state ? isProcessRunning(state.pid) : false;
console.info(chalk.bold(`\nTasks (${tasks.length}) ${workerRunning ? chalk.green("● worker running") : chalk.dim("○ worker stopped")}:\n`));
for (const task of tasks) {
const statusColor = task.status === "active"
? chalk.green
: task.status === "paused"
? chalk.yellow
: task.status === "failed"
? chalk.red
: chalk.dim;
console.info(` ${chalk.bold(task.name)} ${chalk.dim(`(${task.id})`)}`);
console.info(` Status: ${statusColor(task.status)} | Mode: ${task.mode} | Runs: ${task.runCount} | Schedule: ${formatSchedule(task.schedule)}`);
if (task.lastRunAt) {
console.info(` Last run: ${chalk.dim(task.lastRunAt)}`);
}
console.info();
}
await store.shutdown();
}
catch (error) {
spinner.fail(chalk.red("Failed to list tasks"));
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
static async executeGet(argv) {
try {
const store = await TaskCommandFactory.getStore();
const task = await store.get(argv.taskId);
if (!task) {
console.info(chalk.red(`Task not found: ${argv.taskId}`));
await store.shutdown();
process.exit(1);
return;
}
console.info();
console.info(` ${chalk.bold("ID:")} ${task.id}`);
console.info(` ${chalk.bold("Name:")} ${task.name}`);
console.info(` ${chalk.bold("Status:")} ${task.status}`);
console.info(` ${chalk.bold("Mode:")} ${task.mode}`);
console.info(` ${chalk.bold("Schedule:")} ${formatSchedule(task.schedule)}`);
console.info(` ${chalk.bold("Run count:")} ${task.runCount}`);
if (task.maxRuns) {
console.info(` ${chalk.bold("Max runs:")} ${task.maxRuns}`);
}
if (task.provider) {
console.info(` ${chalk.bold("Provider:")} ${task.provider}`);
}
if (task.model) {
console.info(` ${chalk.bold("Model:")} ${task.model}`);
}
if (task.lastRunAt) {
console.info(` ${chalk.bold("Last run:")} ${task.lastRunAt}`);
}
console.info(` ${chalk.bold("Created:")} ${task.createdAt}`);
console.info(` ${chalk.bold("Updated:")} ${task.updatedAt}`);
console.info(` ${chalk.bold("Prompt:")} ${task.prompt.slice(0, 200)}${task.prompt.length > 200 ? "..." : ""}`);
console.info();
await store.shutdown();
}
catch (error) {
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
static async executeRun(argv) {
const spinner = ora("Running task...").start();
try {
const neurolink = await TaskCommandFactory.getNeuroLink();
const manager = neurolink.tasks;
const result = await manager.run(argv.taskId);
if (result.status === "success") {
spinner.succeed(chalk.green(`Task completed in ${result.durationMs}ms`));
if (result.output) {
console.info(`\n${result.output}\n`);
}
}
else {
spinner.fail(chalk.red(`Task failed: ${result.error}`));
}
await manager.shutdown();
}
catch (error) {
spinner.fail(chalk.red("Failed to run task"));
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
static async executePause(argv) {
try {
const store = await TaskCommandFactory.getStore();
const task = await store.get(argv.taskId);
if (!task) {
console.error(chalk.red(`Task not found: ${argv.taskId}`));
await store.shutdown();
process.exit(1);
return;
}
if (task.status !== "active") {
console.error(chalk.red(`Cannot pause task with status: ${task.status}`));
await store.shutdown();
process.exit(1);
return;
}
const updated = await store.update(argv.taskId, { status: "paused" });
console.info(chalk.yellow(`Task "${updated.name}" paused.`));
await store.shutdown();
}
catch (error) {
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
static async executeResume(argv) {
try {
const store = await TaskCommandFactory.getStore();
const task = await store.get(argv.taskId);
if (!task) {
console.error(chalk.red(`Task not found: ${argv.taskId}`));
await store.shutdown();
process.exit(1);
return;
}
if (task.status !== "paused") {
console.error(chalk.red(`Cannot resume task with status: ${task.status}`));
await store.shutdown();
process.exit(1);
return;
}
const updated = await store.update(argv.taskId, { status: "active" });
console.info(chalk.green(`Task "${updated.name}" resumed.`));
await store.shutdown();
}
catch (error) {
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
static async executeUpdate(argv) {
try {
const store = await TaskCommandFactory.getStore();
// Validate mutual exclusivity of schedule flags
const scheduleFlags = [argv.cron, argv.every, argv.at].filter(Boolean);
if (scheduleFlags.length > 1) {
console.error(chalk.red("Only one of --cron, --every, or --at can be used"));
await store.shutdown();
process.exit(1);
return;
}
const updates = {};
if (argv.prompt) {
updates.prompt = argv.prompt;
}
if (argv.mode) {
updates.mode = argv.mode;
}
// Build schedule if any schedule flag is provided
if (argv.cron) {
updates.schedule = { type: "cron", expression: argv.cron };
}
else if (argv.every) {
updates.schedule = {
type: "interval",
every: parseDuration(argv.every),
};
}
else if (argv.at) {
updates.schedule = { type: "once", at: argv.at };
}
if (Object.keys(updates).length === 0) {
console.info(chalk.yellow("No updates specified."));
await store.shutdown();
return;
}
const task = await store.update(argv.taskId, updates);
console.info(chalk.green(`Task "${task.name}" updated.`));
await store.shutdown();
}
catch (error) {
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
static async executeDelete(argv) {
try {
const store = await TaskCommandFactory.getStore();
await store.delete(argv.taskId);
console.info(chalk.green(`Task ${argv.taskId} deleted.`));
await store.shutdown();
}
catch (error) {
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
static async executeLogs(argv) {
try {
const store = await TaskCommandFactory.getStore();
const runs = await store.getRuns(argv.taskId, {
limit: argv.limit,
status: argv.status,
});
if (runs.length === 0) {
console.info(chalk.dim("No runs found."));
await store.shutdown();
return;
}
console.info(chalk.bold(`\nRun history (${runs.length}):\n`));
for (const run of runs) {
const statusIcon = run.status === "success" ? chalk.green("✓") : chalk.red("✗");
const duration = `${run.durationMs}ms`;
console.info(` ${statusIcon} ${chalk.dim(run.runId)} ${chalk.dim(run.timestamp)} ${duration}`);
if (run.error) {
console.info(` ${chalk.red(run.error)}`);
}
if (run.output) {
if (argv.full) {
console.info(` ${run.output}`);
}
else {
const preview = run.output.length > 120
? run.output.slice(0, 120) + "..."
: run.output;
console.info(` ${chalk.dim(preview)}`);
}
}
}
console.info();
await store.shutdown();
}
catch (error) {
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
// ── Start / Stop / Status ──────────────────────────────
static async executeStart(argv) {
// Check if daemon is already running
const existing = workerState.load();
if (existing && isProcessRunning(existing.pid)) {
console.info(chalk.yellow(`Worker already running (PID ${existing.pid}, started ${existing.startedAt}).`));
console.info(chalk.dim(" Run `neurolink task stop` to stop it first."));
return;
}
if (argv.daemon) {
// Spawn detached worker process
await TaskCommandFactory.spawnDaemon();
}
else {
// Foreground worker
await TaskCommandFactory.runForegroundWorker();
}
}
static async executeStop() {
const state = workerState.load();
if (!state) {
console.info(chalk.dim("No worker daemon is running."));
return;
}
if (!isProcessRunning(state.pid)) {
console.info(chalk.dim("Worker daemon is not running (stale state)."));
workerState.clear();
return;
}
try {
process.kill(state.pid, "SIGTERM");
console.info(chalk.green(`Worker daemon stopped (PID ${state.pid}).`));
console.info(chalk.dim(` Logs: ${state.logFile}`));
workerState.clear();
}
catch (error) {
console.error(chalk.red(`Failed to stop worker: ${error instanceof Error ? error.message : String(error)}`));
}
}
static async executeStatus() {
const state = workerState.load();
if (!state) {
console.info(chalk.dim("No worker daemon registered."));
console.info(chalk.dim(" Run `neurolink task start --daemon` to start one."));
return;
}
const running = isProcessRunning(state.pid);
console.info();
console.info(` ${chalk.bold("Status:")} ${running ? chalk.green("● running") : chalk.red("✘ stopped")}`);
console.info(` ${chalk.bold("PID:")} ${state.pid}`);
console.info(` ${chalk.bold("Started:")} ${state.startedAt}`);
if (running) {
const uptimeMs = Date.now() - new Date(state.startedAt).getTime();
console.info(` ${chalk.bold("Uptime:")} ${formatUptime(uptimeMs)}`);
}
console.info(` ${chalk.bold("Logs:")} ${state.logFile}`);
console.info();
if (!running) {
workerState.clear();
}
}
// ── Daemon Spawn ───────────────────────────────────────
static async spawnDaemon() {
const entryScript = process.argv[1];
if (!entryScript) {
console.error(chalk.red("Cannot determine CLI entry point."));
process.exit(1);
return;
}
// Set up log file
ensureStateDir();
const logsDir = join(getNeuroLinkDir(), "logs");
mkdirSync(logsDir, { recursive: true });
const logFile = join(logsDir, "task-worker.log");
const logFd = openSync(logFile, "a");
const args = [entryScript, "task", "_worker"];
const child = spawn(process.execPath, args, {
detached: true,
stdio: ["ignore", logFd, logFd],
cwd: process.cwd(),
env: { ...process.env },
});
child.unref();
const pid = child.pid;
if (!pid) {
console.error(chalk.red("Failed to spawn worker daemon."));
process.exit(1);
return;
}
workerState.save({
pid,
startedAt: new Date().toISOString(),
logFile,
});
console.info(chalk.green(`Worker daemon started (PID ${pid}).`));
console.info(chalk.dim(` Logs: ${logFile}`));
console.info(chalk.dim(" Run `neurolink task stop` to stop it."));
console.info(chalk.dim(" Run `neurolink task status` to check on it."));
}
// ── Foreground Worker ──────────────────────────────────
static async runForegroundWorker() {
const neurolink = await TaskCommandFactory.getNeuroLink();
const manager = neurolink.tasks;
// Trigger initialization and list active tasks
const tasks = await manager.list({ status: "active" });
console.info(chalk.bold("\n NeuroLink Task Worker"));
console.info(chalk.gray(" ─────────────────────────────────"));
console.info(` Active tasks: ${chalk.cyan(String(tasks.length))}`);
for (const t of tasks) {
console.info(` ${chalk.gray("•")} ${t.name} (${chalk.dim(t.id)}) — ${formatSchedule(t.schedule)}`);
}
if (tasks.length === 0) {
console.info(chalk.yellow("\n No active tasks. Create one first with: neurolink task create"));
await manager.shutdown();
return;
}
console.info();
TaskCommandFactory.enterWorkerMode(neurolink, manager);
await new Promise(() => { }); // Block forever until Ctrl+C
}
// ── Hidden _worker process (spawned by --daemon) ───────
static async executeWorkerProcess() {
// This runs in the detached child process
const neurolink = await TaskCommandFactory.getNeuroLink();
const manager = neurolink.tasks;
const tasks = await manager.list({ status: "active" });
console.info(`[task-worker] Started at ${new Date().toISOString()}, active tasks: ${tasks.length}`);
for (const t of tasks) {
console.info(`[task-worker] ${t.name} (${t.id}) — ${formatSchedule(t.schedule)}`);
}
if (tasks.length === 0) {
console.info("[task-worker] No active tasks, exiting.");
workerState.clear();
await manager.shutdown();
return;
}
// Listen for task events (logged to file since stdio goes to log)
const emitter = neurolink.getEventEmitter();
if (emitter) {
emitter.on("task:completed", (result) => {
const r = result;
const preview = r.output
? r.output.length > 200
? r.output.slice(0, 200) + "..."
: r.output
: "(no output)";
console.info(`[task-worker] ✔ ${new Date().toISOString()} ${r.taskId} — ${r.durationMs}ms — ${preview}`);
});
emitter.on("task:failed", (result) => {
const r = result;
console.error(`[task-worker] ✘ ${new Date().toISOString()} ${r.taskId} — ${r.error?.slice(0, 200) || "unknown error"}`);
});
// Autoresearch lifecycle events
TaskCommandFactory.attachAutoresearchEventListeners(emitter);
}
// Graceful shutdown
const shutdown = async () => {
console.info(`[task-worker] Shutting down at ${new Date().toISOString()}`);
await manager.shutdown();
workerState.clear();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
// Block forever
await new Promise(() => { });
}
}
// ── Helpers ─────────────────────────────────────────────
function formatSchedule(schedule) {
if (schedule.type === "cron") {
return `cron "${schedule.expression}"${schedule.timezone ? ` (${schedule.timezone})` : ""}`;
}
if (schedule.type === "interval") {
return `every ${formatDuration(schedule.every)}`;
}
return `once at ${typeof schedule.at === "string" ? schedule.at : schedule.at.toISOString()}`;
}
function formatDuration(ms) {
if (ms < 1000) {
return `${ms}ms`;
}
if (ms < 60_000) {
return `${ms / 1000}s`;
}
if (ms < 3_600_000) {
return `${ms / 60_000}m`;
}
if (ms < 86_400_000) {
return `${ms / 3_600_000}h`;
}
return `${ms / 86_400_000}d`;
}
// ── Arg Types ───────────────────────────────────────────
//# sourceMappingURL=task.js.map