@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
487 lines • 21.5 kB
JavaScript
/**
* AutoResearch CLI Commands for NeuroLink
*
* - neurolink autoresearch init — Initialize autoresearch for a repo
* - neurolink autoresearch status — Show current research state
* - neurolink autoresearch results — Show experiment results
* - neurolink autoresearch run-once — Run one experiment cycle
* - neurolink autoresearch start — Start a scheduled autoresearch task
* - neurolink autoresearch pause — Pause a running autoresearch task
* - neurolink autoresearch resume — Resume a paused autoresearch task
* - neurolink autoresearch stop — Stop and cancel an autoresearch task
* - neurolink autoresearch reset — Reset autoresearch state for a repo
*/
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
import { resolve } from "node:path";
import chalk from "chalk";
import ora from "ora";
import { TASK_DEFAULTS } from "../../lib/types/index.js";
export class AutoresearchCommandFactory {
static createAutoresearchCommands() {
return {
command: "autoresearch <subcommand>",
describe: "Run automated AI-driven research experiments",
builder: (yargs) => {
return yargs
.command("init <repoPath>", "Initialize autoresearch for a repository", (y) => y
.positional("repoPath", {
type: "string",
description: "Path to the repository",
demandOption: true,
})
.option("tag", {
type: "string",
alias: "t",
description: "Run tag (e.g. apr3)",
demandOption: true,
})
.option("target", {
type: "string",
default: "train.py",
description: "Mutable file(s), comma-separated",
})
.option("immutable", {
type: "string",
default: "",
description: "Immutable file(s), comma-separated",
})
.option("run-command", {
type: "string",
description: "Experiment command to execute",
demandOption: true,
})
.option("metric-name", {
type: "string",
default: "val_bpb",
})
.option("metric-pattern", {
type: "string",
default: "^val_bpb:\\s+([\\d.]+)",
})
.option("metric-direction", {
type: "string",
choices: ["lower", "higher"],
default: "lower",
})
.option("timeout", { type: "number", default: 600 })
.option("provider", { type: "string" })
.option("model", { type: "string" }), async (argv) => {
await AutoresearchCommandFactory.executeInit(argv);
})
.command("status [repoPath]", "Show current research state", (y) => y
.positional("repoPath", {
type: "string",
default: ".",
})
.option("format", {
type: "string",
choices: ["text", "json"],
default: "text",
}), async (argv) => {
await AutoresearchCommandFactory.executeStatus(argv);
})
.command("results [repoPath]", "Show experiment results", (y) => y
.positional("repoPath", {
type: "string",
default: ".",
})
.option("last", { type: "number", default: 20 })
.option("format", {
type: "string",
choices: ["text", "json", "table"],
default: "table",
}), async (argv) => {
await AutoresearchCommandFactory.executeResults(argv);
})
.command("run-once [repoPath]", "Run one experiment cycle", (y) => y
.positional("repoPath", {
type: "string",
default: ".",
})
.option("description", {
type: "string",
default: "manual run",
}), async (argv) => {
await AutoresearchCommandFactory.executeRunOnce(argv);
})
.command("start <repoPath>", "Start a scheduled autoresearch task via TaskManager", (y) => y
.positional("repoPath", {
type: "string",
description: "Path to the repository",
demandOption: true,
})
.option("interval", {
type: "number",
default: 300,
description: "Interval between ticks in seconds",
})
.option("max-runs", {
type: "number",
description: "Max experiment ticks (omit for unlimited)",
}), async (argv) => {
await AutoresearchCommandFactory.executeStart(argv);
})
.command("pause <taskId>", "Pause a running autoresearch task", (y) => y.positional("taskId", {
type: "string",
description: "Task ID to pause",
demandOption: true,
}), async (argv) => {
await AutoresearchCommandFactory.executeLifecycle(argv.taskId, "pause");
})
.command("resume <taskId>", "Resume a paused autoresearch task", (y) => y.positional("taskId", {
type: "string",
description: "Task ID to resume",
demandOption: true,
}), async (argv) => {
await AutoresearchCommandFactory.executeLifecycle(argv.taskId, "resume");
})
.command("stop <taskId>", "Stop and cancel an autoresearch task", (y) => y.positional("taskId", {
type: "string",
description: "Task ID to stop",
demandOption: true,
}), async (argv) => {
await AutoresearchCommandFactory.executeLifecycle(argv.taskId, "stop");
})
.command("reset [repoPath]", "Reset autoresearch state for a repository", (y) => y.positional("repoPath", {
type: "string",
default: ".",
}), async (argv) => {
await AutoresearchCommandFactory.executeReset(argv.repoPath);
})
.demandCommand(1, "Please specify an autoresearch subcommand");
},
handler: () => { },
};
}
/**
* Get a direct TaskStore instance for store operations.
* Respects the same backend selection as TaskManager so both paths
* read/write the same store (Redis for bullmq, file for node-timeout).
*/
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;
}
static async executeInit(argv) {
const spinner = ora("Initializing autoresearch...").start();
try {
const repoPath = resolve(argv.repoPath);
if (!existsSync(repoPath)) {
spinner.fail(chalk.red(`Repository not found: ${repoPath}`));
process.exit(1);
}
const { ResearchWorker } = await import("../../lib/autoresearch/worker.js");
const mutablePaths = argv.target
.split(",")
.map((f) => f.trim())
.filter(Boolean);
const immutablePaths = argv.immutable
.split(",")
.map((f) => f.trim())
.filter(Boolean);
const worker = new ResearchWorker({
repoPath,
mutablePaths,
immutablePaths,
runCommand: argv.runCommand,
metric: {
name: argv.metricName,
direction: argv.metricDirection,
pattern: argv.metricPattern,
},
timeoutMs: argv.timeout * 1000,
provider: argv.provider,
model: argv.model,
});
const state = await worker.initialize(argv.tag);
// Persist config for run-once to read
const configDir = resolve(repoPath, ".autoresearch");
mkdirSync(configDir, { recursive: true });
writeFileSync(resolve(configDir, "config.json"), JSON.stringify({
tag: argv.tag,
mutablePaths,
immutablePaths,
runCommand: argv.runCommand,
metric: {
name: argv.metricName,
direction: argv.metricDirection,
pattern: argv.metricPattern,
},
timeoutMs: argv.timeout * 1000,
provider: argv.provider,
model: argv.model,
}, null, 2), "utf-8");
spinner.succeed(chalk.green("Autoresearch initialized"));
console.info(` Branch: ${state.branch} Tag: ${argv.tag} Target: ${mutablePaths.join(", ")}`);
}
catch (error) {
spinner.fail(chalk.red("Failed to initialize"));
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
static async executeStatus(argv) {
const repoPath = resolve(argv.repoPath);
const statePath = resolve(repoPath, ".autoresearch", "state.json");
if (!existsSync(statePath)) {
console.info(chalk.yellow("Not initialized. Run `neurolink autoresearch init` first."));
return;
}
let state;
try {
state = JSON.parse(readFileSync(statePath, "utf-8"));
}
catch (err) {
console.error(chalk.red(`Failed to parse state file: ${statePath}`));
console.error(chalk.dim(err instanceof Error ? err.message : String(err)));
return;
}
if (argv.format === "json") {
console.info(JSON.stringify(state, null, 2));
return;
}
console.info(` Tag: ${state.tag} Branch: ${state.branch} Phase: ${state.currentPhase}`);
console.info(` Runs: ${state.runCount} Keeps: ${state.keepCount} Best: ${state.bestMetric ?? "none"}`);
}
static async executeResults(argv) {
const repoPath = resolve(argv.repoPath);
const configPath = resolve(repoPath, ".autoresearch", "config.json");
let resultsPath = resolve(repoPath, "results.tsv");
if (existsSync(configPath)) {
try {
const config = JSON.parse(readFileSync(configPath, "utf-8"));
if (config.resultsPath) {
resultsPath = resolve(repoPath, config.resultsPath);
}
}
catch {
/* use default */
}
}
if (!existsSync(resultsPath)) {
console.info(chalk.yellow("No results file."));
return;
}
const lines = readFileSync(resultsPath, "utf-8").trim().split("\n");
if (lines.length < 2) {
console.info(chalk.yellow("No results yet."));
return;
}
const rows = lines.slice(1).slice(-argv.last);
if (argv.format === "json") {
console.info(JSON.stringify(rows.map((l) => l.split("\t")), null, 2));
return;
}
console.info(lines[0]);
for (const r of rows) {
console.info(r);
}
}
static async executeRunOnce(argv) {
const spinner = ora("Running experiment...").start();
try {
const repoPath = resolve(argv.repoPath);
const configPath = resolve(repoPath, ".autoresearch", "config.json");
if (!existsSync(configPath)) {
spinner.fail(chalk.red("No config. Run init first."));
process.exit(1);
}
let configRaw;
try {
configRaw = JSON.parse(readFileSync(configPath, "utf-8"));
}
catch (err) {
spinner.fail(chalk.red(`Failed to parse config: ${configPath}`));
console.error(chalk.dim(err instanceof Error ? err.message : String(err)));
process.exit(1);
}
const { ResearchWorker } = await import("../../lib/autoresearch/worker.js");
if (!configRaw.mutablePaths ||
!configRaw.runCommand ||
!configRaw.metric) {
spinner.fail(chalk.red("Config missing required fields (mutablePaths, runCommand, metric)"));
process.exit(1);
}
const worker = new ResearchWorker({
repoPath,
mutablePaths: configRaw.mutablePaths,
runCommand: configRaw.runCommand,
metric: configRaw.metric,
...configRaw,
});
await worker.resume();
const record = await worker.runExperimentCycle(argv.description);
const c = record.status === "keep"
? "green"
: record.status === "discard"
? "yellow"
: "red";
spinner.succeed(chalk[c](`${record.status}: metric=${record.metric ?? "N/A"} commit=${record.commit}`));
}
catch (error) {
spinner.fail(chalk.red(error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
/**
* Start a scheduled autoresearch task via TaskManager.
* Reads config.json from .autoresearch/ and creates a TaskManager task.
*/
static async executeStart(argv) {
const spinner = ora("Starting autoresearch task...").start();
try {
const repoPath = resolve(argv.repoPath);
const configPath = resolve(repoPath, ".autoresearch", "config.json");
if (!existsSync(configPath)) {
spinner.fail(chalk.red("No config. Run `neurolink autoresearch init` first."));
process.exit(1);
}
let configRaw;
try {
configRaw = JSON.parse(readFileSync(configPath, "utf-8"));
}
catch (err) {
spinner.fail(chalk.red(`Failed to parse config: ${configPath}`));
console.error(chalk.dim(err instanceof Error ? err.message : String(err)));
process.exit(1);
}
// Dynamic import to avoid pulling NeuroLink into CLI cold path
const { nanoid } = await import("nanoid");
if (!configRaw.mutablePaths ||
!configRaw.runCommand ||
!configRaw.metric) {
spinner.fail(chalk.red("Config missing required fields (mutablePaths, runCommand, metric)"));
process.exit(1);
}
const store = await AutoresearchCommandFactory.getStore();
const now = new Date().toISOString();
const task = {
id: `task_${nanoid(12)}`,
name: `autoresearch-${repoPath.split("/").pop()}`,
prompt: "Autonomous ML experiment loop",
schedule: { type: "interval", every: argv.interval * 1000 },
mode: "isolated",
type: "autoresearch",
status: "active",
tools: true,
timeout: configRaw.timeoutMs ?? 600_000,
retry: { maxAttempts: 1, backoffMs: [60_000] },
runCount: 0,
createdAt: now,
updatedAt: now,
autoresearch: {
repoPath,
mutablePaths: configRaw.mutablePaths,
runCommand: configRaw.runCommand,
metric: configRaw.metric,
...(configRaw.immutablePaths?.length
? { immutablePaths: configRaw.immutablePaths }
: {}),
...(configRaw.programPath
? { programPath: configRaw.programPath }
: {}),
...(configRaw.resultsPath
? { resultsPath: configRaw.resultsPath }
: {}),
...(configRaw.statePath ? { statePath: configRaw.statePath } : {}),
...(configRaw.logPath ? { logPath: configRaw.logPath } : {}),
...(configRaw.branchPrefix
? { branchPrefix: configRaw.branchPrefix }
: {}),
...(configRaw.memoryMetric
? { memoryMetric: configRaw.memoryMetric }
: {}),
...(configRaw.timeoutMs ? { timeoutMs: configRaw.timeoutMs } : {}),
...(configRaw.provider ? { provider: configRaw.provider } : {}),
...(configRaw.model ? { model: configRaw.model } : {}),
...(configRaw.thinkingLevel
? { thinkingLevel: configRaw.thinkingLevel }
: {}),
...(configRaw.maxExperiments
? { maxExperiments: configRaw.maxExperiments }
: {}),
},
...(argv.maxRuns ? { maxRuns: argv.maxRuns } : {}),
};
await store.save(task);
await store.shutdown();
spinner.succeed(chalk.green(`Autoresearch task created: ${task.id}`));
console.info(` Interval: ${argv.interval}s Max runs: ${argv.maxRuns ?? "unlimited"}`);
console.info(chalk.dim(" Note: Start the task worker with `neurolink task start` to begin execution."));
}
catch (error) {
spinner.fail(chalk.red(error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
/**
* Lifecycle operations: pause, resume, stop.
* Reads from the file task store and updates task status.
*/
static async executeLifecycle(taskId, action) {
const spinner = ora(`${action}ing task ${taskId}...`).start();
try {
const store = await AutoresearchCommandFactory.getStore();
const task = await store.get(taskId);
if (!task) {
spinner.fail(chalk.red(`Task not found: ${taskId}`));
process.exit(1);
}
if (task.type !== "autoresearch") {
spinner.fail(chalk.red(`Task ${taskId} is not an autoresearch task (type: ${task.type})`));
process.exit(1);
}
let newStatus;
if (action === "pause") {
if (task.status !== "active") {
spinner.fail(chalk.red(`Cannot pause task with status: ${task.status}`));
process.exit(1);
}
newStatus = "paused";
}
else if (action === "resume") {
if (task.status !== "paused") {
spinner.fail(chalk.red(`Cannot resume task with status: ${task.status}`));
process.exit(1);
}
newStatus = "active";
}
else {
// stop
newStatus = "cancelled";
}
await store.update(taskId, {
status: newStatus,
});
await store.shutdown();
spinner.succeed(chalk.green(`Task ${taskId} → ${newStatus}`));
}
catch (error) {
spinner.fail(chalk.red(error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
/**
* Reset autoresearch state by removing the .autoresearch directory.
*/
static async executeReset(repoPath) {
const resolved = resolve(repoPath);
const arDir = resolve(resolved, ".autoresearch");
if (!existsSync(arDir)) {
console.info(chalk.yellow("No .autoresearch directory found."));
return;
}
rmSync(arDir, { recursive: true, force: true });
console.info(chalk.green(`Reset autoresearch state for ${resolved}`));
}
}
//# sourceMappingURL=autoresearch.js.map