@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
647 lines • 27.9 kB
JavaScript
/**
* Serve CLI Commands for NeuroLink
* Simplified HTTP server management commands for Server Adapters feature
*
* Usage:
* neurolink serve --framework <hono|express|fastify|koa> --port <n>
* neurolink serve --config <file>
* neurolink serve --cors --rate-limit
* neurolink serve status
*/
import chalk from "chalk";
import fs from "fs";
import ora from "ora";
import path from "path";
import { NeuroLink } from "../../lib/neurolink.js";
import { ConfigurationError, ServerStartError, } from "../../lib/server/errors.js";
import { withTimeout } from "../../lib/utils/errorHandling.js";
import { logger } from "../../lib/utils/logger.js";
import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
// ============================================
// State Management
// ============================================
// Use StateFileManager for serve state persistence
const serveStateManager = new StateFileManager("serve-state.json");
function saveServeState(state) {
serveStateManager.save(state);
}
function loadServeState() {
return serveStateManager.load();
}
function clearServeState() {
serveStateManager.clear();
}
function loadConfigFile(configPath) {
const absolutePath = path.isAbsolute(configPath)
? configPath
: path.resolve(process.cwd(), configPath);
if (!fs.existsSync(absolutePath)) {
throw new ConfigurationError(`Config file not found: ${absolutePath}`, {
configPath,
absolutePath,
});
}
const content = fs.readFileSync(absolutePath, "utf8");
// Support both JSON and JS/TS module format (for JSON only at runtime)
if (absolutePath.endsWith(".json")) {
return JSON.parse(content);
}
throw new ConfigurationError("Only JSON config files are supported. Use .json extension.", {
configPath,
absolutePath,
});
}
// ============================================
// Watch Mode Utilities
// ============================================
/**
* Directories to watch for changes in watch mode
*/
const WATCH_DIRS = ["src", "lib"];
/**
* File extensions to watch for changes
*/
const WATCH_EXTENSIONS = [".ts", ".js", ".json"];
/**
* Debounce time for file changes (ms)
*/
const WATCH_DEBOUNCE_MS = 500;
/**
* Create a file watcher for watch mode
* Returns a cleanup function to stop watching
*/
function createFileWatcher(onRestart, quiet) {
const watchers = [];
let debounceTimer = null;
let isRestarting = false;
const handleChange = (eventType, filename) => {
// Skip if no filename or if it doesn't match our extensions
if (!filename) {
return;
}
const ext = path.extname(filename);
if (!WATCH_EXTENSIONS.includes(ext)) {
return;
}
// Debounce rapid changes
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(async () => {
if (isRestarting) {
return;
}
isRestarting = true;
if (!quiet) {
logger.always("");
logger.always(chalk.yellow(`File changed: ${filename}. Restarting server...`));
}
try {
await onRestart();
}
finally {
isRestarting = false;
}
}, WATCH_DEBOUNCE_MS);
};
// Watch each directory
const cwd = process.cwd();
for (const dir of WATCH_DIRS) {
const watchPath = path.join(cwd, dir);
if (fs.existsSync(watchPath)) {
try {
const watcher = fs.watch(watchPath, { recursive: true }, handleChange);
watchers.push(watcher);
}
catch {
// Ignore errors for directories that can't be watched
}
}
}
// Return cleanup function
return () => {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
for (const watcher of watchers) {
watcher.close();
}
};
}
// ============================================
// Serve Command Factory
// ============================================
/**
* Serve CLI command factory
*/
export class ServeCommandFactory {
/**
* Create the main serve command
*/
static createServeCommands() {
return {
command: "serve [subcommand]",
describe: "Start NeuroLink HTTP server with server adapters",
builder: (yargs) => {
return yargs
.command("status", "Show server status", (yargs) => ServeCommandFactory.buildStatusOptions(yargs), (argv) => ServeCommandFactory.executeStatus(argv))
.command("voice", "Start the real-time voice assistant server (OpenAI Realtime / Gemini Live)", (yargs) => yargs.option("port", {
alias: "p",
type: "number",
default: 3000,
describe: "Port to listen on",
}), async (argv) => {
const { startVoiceServer } = await import("../../lib/server/voice/voiceServerApp.js");
await startVoiceServer(argv.port);
})
.option("port", {
type: "number",
alias: "p",
default: 3000,
description: "Port to listen on",
})
.option("host", {
type: "string",
alias: "H",
default: "0.0.0.0",
description: "Host to bind to",
})
.option("framework", {
type: "string",
alias: "f",
choices: ["hono", "express", "fastify", "koa"],
default: "hono",
description: "Web framework to use (hono recommended)",
})
.option("basePath", {
type: "string",
alias: "b",
default: "/api",
description: "Base path for all routes",
})
.option("cors", {
type: "boolean",
default: true,
description: "Enable CORS middleware",
})
.option("rate-limit", {
type: "number",
alias: "rateLimit",
default: 100,
description: "Rate limit (requests per 15 min window, 0 to disable)",
})
.option("swagger", {
type: "boolean",
default: false,
description: "Enable OpenAPI/Swagger documentation",
})
.option("config", {
type: "string",
alias: "c",
description: "Path to server config file (JSON)",
})
.option("watch", {
type: "boolean",
alias: "w",
default: false,
description: "Watch mode (restart server on file changes)",
})
.option("quiet", {
type: "boolean",
alias: "q",
default: false,
description: "Suppress non-essential output",
})
.option("debug", {
type: "boolean",
alias: "d",
default: false,
description: "Enable debug output",
})
.example("neurolink serve", "Start server with default settings (Hono on port 3000)")
.example("neurolink serve --framework express --port 8080", "Start Express server on port 8080")
.example("neurolink serve --config server.config.json", "Start server with config file")
.example("neurolink serve --cors --rate-limit 50", "Start server with CORS and rate limiting (50 req/15min)")
.example("neurolink serve --swagger", "Start server with OpenAPI documentation enabled")
.example("neurolink serve --watch", "Start server in watch mode (restart on changes)")
.example("neurolink serve status", "Show server status")
.help();
},
handler: async (argv) => {
// If subcommand is provided (like 'status'), it will be handled by the subcommand
// Otherwise, start the server
if (!argv.subcommand || argv.subcommand === "serve") {
await ServeCommandFactory.executeServe(argv);
}
},
};
}
static buildStatusOptions(yargs) {
return yargs
.option("format", {
type: "string",
choices: ["text", "json"],
default: "text",
description: "Output format",
})
.option("quiet", {
type: "boolean",
alias: "q",
default: false,
description: "Suppress non-essential output",
})
.example("neurolink serve status", "Show server status")
.example("neurolink serve status --format json", "Show status as JSON");
}
// ============================================
// Command Executors
// ============================================
static async executeServe(argv) {
const spinner = argv.quiet
? null
: ora("Starting NeuroLink server...").start();
try {
// Check if server is already running
ServeCommandFactory.guardAlreadyRunning(spinner);
// Load config file if provided
const fileConfig = ServeCommandFactory.loadFileConfig(argv, spinner);
// Merge CLI args with file config (CLI takes precedence)
const port = argv.port ?? fileConfig.port ?? 3000;
const host = argv.host ?? fileConfig.host ?? "0.0.0.0";
const framework = argv.framework ?? fileConfig.framework ?? "hono";
const basePath = argv.basePath ?? fileConfig.basePath ?? "/api";
// Build server adapter config from merged values
const serverConfig = ServeCommandFactory.buildServerConfig(argv, fileConfig, {
port,
host,
basePath,
});
if (spinner) {
spinner.text = `Creating ${framework} server...`;
}
// Create, register routes, initialize and start server
const serverRef = await ServeCommandFactory.createAndStartServer({ framework, serverConfig, basePath, port, host }, spinner);
// Save state and print startup banner
ServeCommandFactory.saveAndPrintStartupInfo({ argv, port, host, framework, basePath, serverConfig }, spinner);
// Set up watch mode if enabled
const stopWatcher = ServeCommandFactory.setupWatchMode(argv, {
serverRef,
framework,
serverConfig,
basePath,
port,
host,
});
// Register signal handlers for graceful shutdown
ServeCommandFactory.registerSignalHandlers(serverRef, stopWatcher);
}
catch (error) {
ServeCommandFactory.handleStartupError(error, argv, spinner);
}
}
// ============================================
// executeServe Helpers
// ============================================
/**
* Guard against a server that is already running. Exits the process if so.
*/
static guardAlreadyRunning(spinner) {
const existingState = loadServeState();
if (existingState && isProcessRunning(existingState.pid)) {
if (spinner) {
spinner.fail(chalk.red(`Server already running on port ${existingState.port} (PID: ${existingState.pid})`));
}
logger.always(chalk.yellow("Use 'neurolink server stop' or kill the process to stop it first"));
process.exit(1);
}
}
/**
* Load a config file if one was specified in argv. Returns empty object otherwise.
*/
static loadFileConfig(argv, spinner) {
if (!argv.config) {
return {};
}
try {
const config = loadConfigFile(argv.config);
if (spinner) {
spinner.text = `Loading config from ${argv.config}...`;
}
return config;
}
catch (configError) {
if (spinner) {
spinner.fail(chalk.red("Failed to load config file"));
}
logger.error(chalk.red(`Error: ${configError instanceof Error ? configError.message : String(configError)}`));
process.exit(1);
}
}
/**
* Merge CLI args with file config to produce a ServerAdapterConfig.
*/
static buildServerConfig(argv, fileConfig, merged) {
const defaultConfig = {
cors: { enabled: true },
rateLimit: { enabled: true, maxRequests: 100 },
};
return {
port: merged.port,
host: merged.host,
basePath: merged.basePath,
cors: {
...defaultConfig.cors,
...(fileConfig.cors || {}),
enabled: argv.cors !== undefined
? argv.cors
: (fileConfig.cors?.enabled ?? defaultConfig.cors.enabled),
},
rateLimit: {
...defaultConfig.rateLimit,
...(fileConfig.rateLimit || {}),
enabled: argv.rateLimit !== undefined
? argv.rateLimit > 0
: (fileConfig.rateLimit?.enabled ??
defaultConfig.rateLimit.enabled),
maxRequests: argv.rateLimit !== undefined
? argv.rateLimit
: (fileConfig.rateLimit?.maxRequests ??
defaultConfig.rateLimit.maxRequests),
},
bodyParser: fileConfig.bodyParser,
logging: fileConfig.logging,
timeout: fileConfig.timeout,
enableMetrics: fileConfig.enableMetrics ?? true,
enableSwagger: argv.swagger !== undefined
? argv.swagger
: (fileConfig.enableSwagger ?? false),
disableBuiltInHealth: true, // We register health routes separately
};
}
/**
* Create server, register routes, initialize and start it.
* Returns a mutable reference wrapper so signal handlers always access the current server.
*/
static async createAndStartServer(opts, spinner) {
const { createServer, registerAllRoutes } = await import("../../lib/server/index.js");
const neurolink = new NeuroLink();
const serverRef = {
current: await createServer(neurolink, {
framework: opts.framework,
config: opts.serverConfig,
}),
};
registerAllRoutes(serverRef.current, opts.basePath);
if (spinner) {
spinner.text = "Initializing server...";
}
await withTimeout(serverRef.current.initialize(), 30000, new ServerStartError("Server initialization timed out after 30 seconds", undefined, opts.port, opts.host));
await withTimeout(serverRef.current.start(), 30000, new ServerStartError("Server startup timed out after 30 seconds", undefined, opts.port, opts.host));
return serverRef;
}
/**
* Save server state and print the startup info banner.
*/
static saveAndPrintStartupInfo(opts, spinner) {
const state = {
pid: process.pid,
port: opts.port,
host: opts.host,
framework: opts.framework,
startTime: new Date().toISOString(),
basePath: opts.basePath,
configFile: opts.argv.config,
};
saveServeState(state);
if (spinner) {
spinner.succeed(chalk.green("NeuroLink server started successfully"));
}
const url = `http://${opts.host === "0.0.0.0" ? "localhost" : opts.host}:${opts.port}`;
const corsEnabled = opts.argv.cors ?? opts.serverConfig.cors?.enabled ?? true;
const rateLimitValue = opts.argv.rateLimit ?? opts.serverConfig.rateLimit?.maxRequests ?? 100;
const rateLimitEnabled = rateLimitValue > 0;
const swaggerEnabled = opts.argv.swagger ?? opts.serverConfig.enableSwagger ?? false;
ServeCommandFactory.printStartupBanner({
url,
framework: opts.framework,
basePath: opts.basePath,
pid: state.pid,
configFile: opts.argv.config,
corsEnabled,
rateLimitEnabled,
rateLimitValue,
swaggerEnabled,
watchEnabled: opts.argv.watch ?? false,
});
return state;
}
/**
* Print the server startup banner with server info, middleware status and endpoints.
*/
static printStartupBanner(info) {
logger.always("");
logger.always(chalk.bold.cyan("NeuroLink Server"));
logger.always(chalk.gray("=".repeat(50)));
logger.always("");
logger.always(` ${chalk.bold("URL:")} ${chalk.cyan(info.url)}`);
logger.always(` ${chalk.bold("Framework:")} ${chalk.cyan(info.framework)}`);
logger.always(` ${chalk.bold("Base Path:")} ${chalk.cyan(info.basePath)}`);
logger.always(` ${chalk.bold("PID:")} ${chalk.cyan(info.pid)}`);
if (info.configFile) {
logger.always(` ${chalk.bold("Config:")} ${chalk.cyan(info.configFile)}`);
}
logger.always("");
logger.always(chalk.bold("Middleware:"));
logger.always(` CORS: ${info.corsEnabled ? chalk.green("enabled") : chalk.yellow("disabled")}`);
logger.always(` Rate Limit: ${info.rateLimitEnabled ? chalk.green(`enabled (${info.rateLimitValue} req/15min)`) : chalk.yellow("disabled")}`);
logger.always(` Swagger: ${info.swaggerEnabled ? chalk.green("enabled") : chalk.yellow("disabled")}`);
if (info.watchEnabled) {
logger.always(` Watch Mode: ${chalk.green("enabled")}`);
}
logger.always("");
logger.always(chalk.bold("Available Endpoints:"));
logger.always(chalk.gray(" Health & Monitoring:"));
logger.always(` ${chalk.green("GET")} ${info.basePath}/health`);
logger.always(` ${chalk.green("GET")} ${info.basePath}/ready`);
logger.always(` ${chalk.green("GET")} ${info.basePath}/metrics`);
logger.always("");
logger.always(chalk.gray(" Agent API:"));
logger.always(` ${chalk.blue("POST")} ${info.basePath}/agent/execute`);
logger.always(` ${chalk.blue("POST")} ${info.basePath}/agent/stream`);
logger.always(` ${chalk.green("GET")} ${info.basePath}/agent/providers`);
logger.always("");
logger.always(chalk.gray(" Tools & MCP:"));
logger.always(` ${chalk.green("GET")} ${info.basePath}/tools`);
logger.always(` ${chalk.blue("POST")} ${info.basePath}/tools/:name/execute`);
logger.always(` ${chalk.green("GET")} ${info.basePath}/mcp/servers`);
logger.always("");
logger.always(chalk.gray(" Memory:"));
logger.always(` ${chalk.green("GET")} ${info.basePath}/memory/sessions`);
logger.always(` ${chalk.green("GET")} ${info.basePath}/memory/sessions/:id`);
if (info.swaggerEnabled) {
logger.always("");
logger.always(chalk.gray(" OpenAPI Documentation:"));
logger.always(` ${chalk.green("GET")} ${info.basePath}/openapi.json`);
logger.always(` ${chalk.cyan("INFO")} Swagger UI available at ${info.url}${info.basePath}/docs`);
}
logger.always("");
logger.always(chalk.gray("Press Ctrl+C to stop the server"));
logger.always("");
}
/**
* Set up watch mode if enabled. Returns the stop-watcher function, or null if not enabled.
*/
static setupWatchMode(argv, ctx) {
if (!argv.watch) {
return null;
}
const restartServer = async () => {
try {
// Stop current server with timeout
await withTimeout(ctx.serverRef.current.stop(), 30000, new ServerStartError("Server stop timed out during restart", undefined, ctx.port, ctx.host));
// Re-import server module with cache busting for watch mode
const timestamp = Date.now();
const { createServer: createNewServer, registerAllRoutes: registerNewRoutes, } = await import(`../../lib/server/index.js?t=${timestamp}`);
// Create new server
const newServer = await createNewServer(new NeuroLink(), {
framework: ctx.framework,
config: ctx.serverConfig,
});
registerNewRoutes(newServer, ctx.basePath);
// Initialize and start with timeouts
await withTimeout(newServer.initialize(), 30000, new ServerStartError("Server initialization timed out during restart", undefined, ctx.port, ctx.host));
await withTimeout(newServer.start(), 30000, new ServerStartError("Server startup timed out during restart", undefined, ctx.port, ctx.host));
// Update the reference so signal handlers use the new server instance
ctx.serverRef.current = newServer;
logger.always(chalk.green("Server restarted successfully"));
}
catch (restartError) {
logger.error(chalk.red(`Error restarting server: ${restartError instanceof Error ? restartError.message : String(restartError)}`));
}
};
const stopWatcher = createFileWatcher(restartServer, argv.quiet ?? false);
logger.always(chalk.gray("Watching for file changes in src/ and lib/..."));
logger.always("");
return stopWatcher;
}
/**
* Register SIGINT and SIGTERM handlers for graceful shutdown.
*/
static registerSignalHandlers(serverRef, stopWatcher) {
process.on("SIGINT", async () => {
logger.always("");
logger.always(chalk.yellow("Shutting down server..."));
try {
if (stopWatcher) {
stopWatcher();
}
await serverRef.current.stop();
clearServeState();
logger.always(chalk.green("Server stopped gracefully"));
process.exit(0);
}
catch (error) {
logger.error(chalk.red(`Error stopping server: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
});
process.on("SIGTERM", async () => {
try {
if (stopWatcher) {
stopWatcher();
}
await withTimeout(serverRef.current.stop(), 30000, new Error("Server stop timed out during SIGTERM"));
clearServeState();
process.exit(0);
}
catch (error) {
logger.error(chalk.red(`Error stopping server: ${error instanceof Error ? error.message : String(error)}`));
clearServeState();
process.exit(1);
}
});
}
/**
* Handle errors during server startup: print error, troubleshooting tips, and exit.
*/
static handleStartupError(error, argv, spinner) {
if (spinner) {
spinner.fail(chalk.red("Failed to start server"));
}
logger.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
if (argv.debug && error instanceof Error && error.stack) {
logger.error(chalk.gray(error.stack));
}
logger.always("");
logger.always(chalk.bold("Troubleshooting:"));
logger.always(" 1. Check if the port is already in use");
logger.always(" 2. Verify the framework is installed (npm install hono/express/fastify/koa)");
logger.always(" 3. Check your config file format if using --config");
logger.always(" 4. Run with --debug for more information");
logger.always("");
process.exit(1);
}
static async executeStatus(argv) {
try {
const state = loadServeState();
const status = {
running: false,
pid: null,
port: null,
host: null,
framework: null,
basePath: null,
uptime: null,
startTime: null,
configFile: null,
url: null,
};
if (state && isProcessRunning(state.pid)) {
status.running = true;
status.pid = state.pid;
status.port = state.port;
status.host = state.host;
status.framework = state.framework;
status.basePath = state.basePath;
status.startTime = state.startTime;
status.uptime = Date.now() - new Date(state.startTime).getTime();
status.configFile = state.configFile ?? null;
status.url = `http://${state.host === "0.0.0.0" ? "localhost" : state.host}:${state.port}`;
}
if (argv.format === "json") {
logger.always(JSON.stringify(status, null, 2));
return;
}
// Text format
logger.always("");
logger.always(chalk.bold.cyan("NeuroLink Server Status"));
logger.always(chalk.gray("=".repeat(50)));
logger.always("");
if (status.running) {
logger.always(` ${chalk.bold("Status:")} ${chalk.green("RUNNING")}`);
logger.always(` ${chalk.bold("PID:")} ${chalk.cyan(status.pid)}`);
logger.always(` ${chalk.bold("URL:")} ${chalk.cyan(status.url)}`);
logger.always(` ${chalk.bold("Framework:")} ${chalk.cyan(status.framework)}`);
logger.always(` ${chalk.bold("Base Path:")} ${chalk.cyan(status.basePath)}`);
logger.always(` ${chalk.bold("Started:")} ${chalk.cyan(status.startTime)}`);
logger.always(` ${chalk.bold("Uptime:")} ${chalk.cyan(formatUptime(status.uptime ?? 0))}`);
if (status.configFile) {
logger.always(` ${chalk.bold("Config:")} ${chalk.cyan(status.configFile)}`);
}
}
else {
logger.always(` ${chalk.bold("Status:")} ${chalk.yellow("NOT RUNNING")}`);
logger.always("");
logger.always(chalk.gray(" Start the server with: neurolink serve"));
logger.always(chalk.gray(" Or with custom options: neurolink serve --port 8080 --framework express"));
}
logger.always("");
}
catch (error) {
logger.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
}
export default ServeCommandFactory;
//# sourceMappingURL=serve.js.map