UNPKG

@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
/** * 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