@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
750 lines • 30.2 kB
JavaScript
/**
* Server CLI Commands for NeuroLink
* Implements HTTP server management commands
*/
import chalk from "chalk";
import ora from "ora";
import fs from "fs";
import path from "path";
import { logger } from "../../lib/utils/logger.js";
import { NeuroLink } from "../../lib/neurolink.js";
import { withTimeout } from "../../lib/utils/errorHandling.js";
import { isProcessRunning, formatUptime, ensureStateDir, getNeuroLinkDir, } from "../utils/serverUtils.js";
// ============================================
// State Management
// ============================================
const STATE_FILE = path.join(getNeuroLinkDir(), "server-state.json");
function saveServerState(state) {
ensureStateDir();
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}
function loadServerState() {
try {
if (fs.existsSync(STATE_FILE)) {
const content = fs.readFileSync(STATE_FILE, "utf8");
return JSON.parse(content);
}
}
catch {
// Ignore errors
}
return null;
}
function clearServerState() {
try {
if (fs.existsSync(STATE_FILE)) {
fs.unlinkSync(STATE_FILE);
}
}
catch {
// Ignore errors
}
}
// ============================================
// Config Management
// ============================================
const CONFIG_FILE = path.join(getNeuroLinkDir(), "server-config.json");
function loadServerConfig() {
const defaults = {
defaultPort: 3000,
defaultHost: "0.0.0.0",
defaultFramework: "hono",
defaultBasePath: "/api",
cors: { enabled: true },
rateLimit: { enabled: true, windowMs: 60000, maxRequests: 100 },
swagger: { enabled: true, path: "/docs" },
};
try {
if (fs.existsSync(CONFIG_FILE)) {
const content = fs.readFileSync(CONFIG_FILE, "utf8");
return { ...defaults, ...JSON.parse(content) };
}
}
catch {
// Return defaults on error
}
return defaults;
}
function saveServerConfig(config) {
ensureStateDir();
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
}
// Helper to get nested config values
function getNestedValue(obj, path) {
const keys = path.split(".");
let value = obj;
for (const key of keys) {
if (value && typeof value === "object" && key in value) {
value = value[key];
}
else {
return undefined;
}
}
return value;
}
// Helper to set nested config values
function setNestedValue(obj, path, value) {
const keys = path.split(".");
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
// Guard against prototype pollution - abort entire operation
if (key === "__proto__" || key === "constructor" || key === "prototype") {
logger.warn(chalk.yellow(`[ServerConfig] Blocked dangerous config key: ${path}`));
return; // Abort - don't continue traversal
}
if (!(key in current)) {
current[key] = {};
}
current = current[key];
}
const finalKey = keys[keys.length - 1];
// Guard against prototype pollution for final key
if (finalKey === "__proto__" ||
finalKey === "constructor" ||
finalKey === "prototype") {
logger.warn(chalk.yellow(`[ServerConfig] Blocked dangerous config key: ${path}`));
return; // Skip dangerous keys
}
current[finalKey] = value;
}
// Helper to parse config values
function parseConfigValue(value) {
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
if (!isNaN(Number(value))) {
return Number(value);
}
return value;
}
// ============================================
// Server Command Factory
// ============================================
/**
* Server CLI command factory
*/
export class ServerCommandFactory {
/**
* Create the main server command with subcommands
*/
static createServerCommands() {
return {
command: "server <subcommand>",
describe: "Manage NeuroLink HTTP server",
builder: (yargs) => {
return yargs
.command("start", "Start the HTTP server", (yargs) => this.buildStartOptions(yargs), (argv) => this.executeStart(argv))
.command("stop", "Stop the running server", (yargs) => this.buildStopOptions(yargs), (argv) => this.executeStop(argv))
.command("status", "Show server status", (yargs) => this.buildStatusOptions(yargs), (argv) => this.executeStatus(argv))
.command("openapi", "Generate OpenAPI specification", (yargs) => this.buildOpenAPIOptions(yargs), (argv) => this.executeOpenAPI(argv))
.command("routes", "List all registered server routes", (yargs) => this.buildRoutesOptions(yargs), (argv) => this.executeRoutes(argv))
.command("config", "Show or modify server configuration", (yargs) => this.buildConfigOptions(yargs), (argv) => this.executeConfig(argv))
.demandCommand(1, "Please specify a server subcommand")
.help();
},
handler: () => {
// No-op handler as subcommands handle everything
},
};
}
// ============================================
// Option Builders
// ============================================
static buildStartOptions(yargs) {
return yargs
.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",
})
.option("basePath", {
type: "string",
default: "/api",
description: "Base path for all routes",
})
.option("cors", {
type: "boolean",
default: true,
description: "Enable CORS",
})
.option("rateLimit", {
type: "boolean",
default: true,
description: "Enable rate limiting",
})
.option("quiet", {
type: "boolean",
alias: "q",
default: false,
description: "Suppress non-essential output",
})
.option("debug", {
type: "boolean",
default: false,
description: "Enable debug output",
})
.example("neurolink server start", "Start server on default port 3000")
.example("neurolink server start -p 8080", "Start server on port 8080")
.example("neurolink server start --framework express", "Start with Express");
}
static buildStopOptions(yargs) {
return yargs
.option("force", {
type: "boolean",
default: false,
description: "Force stop even if server is not responding",
})
.option("quiet", {
type: "boolean",
alias: "q",
default: false,
description: "Suppress non-essential output",
})
.example("neurolink server stop", "Stop the running server");
}
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 server status", "Show server status")
.example("neurolink server status --format json", "Show status as JSON");
}
static buildOpenAPIOptions(yargs) {
return yargs
.option("output", {
type: "string",
alias: "o",
description: "Output file path (default: stdout)",
})
.option("format", {
type: "string",
choices: ["json", "yaml"],
default: "json",
description: "Output format",
})
.option("basePath", {
type: "string",
default: "/api",
description: "Base path for all routes",
})
.option("title", {
type: "string",
description: "API title",
})
.option("version", {
type: "string",
description: "API version",
})
.option("quiet", {
type: "boolean",
alias: "q",
default: false,
description: "Suppress non-essential output",
})
.example("neurolink server openapi", "Generate OpenAPI spec to stdout")
.example("neurolink server openapi -o openapi.json", "Save to file")
.example("neurolink server openapi --format yaml -o openapi.yaml", "Generate YAML spec");
}
static buildRoutesOptions(yargs) {
return yargs
.option("format", {
type: "string",
choices: ["text", "json", "table"],
default: "table",
description: "Output format",
})
.option("group", {
type: "string",
choices: ["agent", "tool", "mcp", "memory", "health", "all"],
default: "all",
description: "Filter by route group",
})
.option("method", {
type: "string",
choices: ["GET", "POST", "PUT", "DELETE", "PATCH", "all"],
default: "all",
description: "Filter by HTTP method",
})
.option("quiet", {
type: "boolean",
alias: "q",
default: false,
description: "Suppress non-essential output",
})
.example("neurolink server routes", "List all registered routes")
.example("neurolink server routes --group agent", "List agent routes only")
.example("neurolink server routes --format json", "Output as JSON");
}
static buildConfigOptions(yargs) {
return yargs
.option("get", {
type: "string",
description: "Get a specific config value (e.g., defaultPort)",
})
.option("set", {
type: "string",
description: "Set a config value (format: key=value)",
})
.option("reset", {
type: "boolean",
default: false,
description: "Reset configuration to defaults",
})
.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 server config", "Show all configuration")
.example("neurolink server config --get defaultPort", "Get specific value")
.example("neurolink server config --set defaultPort=8080", "Set config value")
.example("neurolink server config --reset", "Reset to defaults");
}
// ============================================
// Command Executors
// ============================================
static async executeStart(argv) {
const spinner = argv.quiet ? null : ora("Starting server...").start();
try {
// Check if server is already running
const existingState = loadServerState();
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' to stop it first"));
process.exit(1);
}
// Create NeuroLink instance
const neurolink = new NeuroLink();
// Dynamically import server module
const { createServer, registerAllRoutes } = await import("../../lib/server/index.js");
const framework = (argv.framework ?? "hono");
// Create server
const server = await createServer(neurolink, {
framework,
config: {
port: argv.port ?? 3000,
host: argv.host ?? "0.0.0.0",
basePath: argv.basePath ?? "/api",
cors: {
enabled: argv.cors ?? true,
},
rateLimit: {
enabled: argv.rateLimit ?? true,
},
disableBuiltInHealth: true, // We register health routes separately
},
});
// Register all routes
registerAllRoutes(server, argv.basePath ?? "/api");
// Initialize and start with timeout
await withTimeout(server.initialize(), 30000, new Error("Server initialization timed out after 30 seconds"));
await withTimeout(server.start(), 30000, new Error("Server startup timed out after 30 seconds"));
// Save state
const state = {
pid: process.pid,
port: argv.port ?? 3000,
host: argv.host ?? "0.0.0.0",
framework,
startTime: new Date().toISOString(),
basePath: argv.basePath ?? "/api",
};
saveServerState(state);
if (spinner) {
spinner.succeed(chalk.green("Server started successfully"));
}
const url = `http://${state.host === "0.0.0.0" ? "localhost" : state.host}:${state.port}`;
logger.always(chalk.bold("\nServer Information:"));
logger.always(` URL: ${chalk.cyan(url)}`);
logger.always(` Framework: ${chalk.cyan(framework)}`);
logger.always(` Base Path: ${chalk.cyan(state.basePath)}`);
logger.always(` PID: ${chalk.cyan(state.pid)}`);
logger.always(chalk.bold("\nAvailable Endpoints:"));
logger.always(` ${chalk.green("GET")} ${state.basePath}/health`);
logger.always(` ${chalk.green("GET")} ${state.basePath}/ready`);
logger.always(` ${chalk.green("GET")} ${state.basePath}/metrics`);
logger.always(` ${chalk.blue("POST")} ${state.basePath}/agent/execute`);
logger.always(` ${chalk.blue("POST")} ${state.basePath}/agent/stream`);
logger.always(` ${chalk.green("GET")} ${state.basePath}/agent/providers`);
logger.always(` ${chalk.green("GET")} ${state.basePath}/tools`);
logger.always(` ${chalk.green("GET")} ${state.basePath}/mcp/servers`);
logger.always(chalk.gray("\nPress Ctrl+C to stop the server"));
// Keep process running
process.on("SIGINT", async () => {
logger.always(chalk.yellow("\nShutting down server..."));
try {
await server.stop();
clearServerState();
logger.always(chalk.green("Server stopped"));
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 {
await server.stop();
clearServerState();
process.exit(0);
}
catch (error) {
logger.error(chalk.red(`Error stopping server: ${error instanceof Error ? error.message : String(error)}`));
clearServerState();
process.exit(1);
}
});
}
catch (error) {
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) {
logger.error(chalk.gray(error instanceof Error ? (error.stack ?? "") : String(error)));
}
process.exit(1);
}
}
static async executeStop(argv) {
const spinner = argv.quiet ? null : ora("Stopping server...").start();
try {
const state = loadServerState();
if (!state) {
if (spinner) {
spinner.warn(chalk.yellow("No server state found"));
}
logger.always(chalk.yellow("Server may not be running or was started in a different way"));
return;
}
if (!isProcessRunning(state.pid)) {
if (spinner) {
spinner.warn(chalk.yellow("Server process not found"));
}
logger.always(chalk.yellow(`Server process ${state.pid} is not running`));
clearServerState();
return;
}
// Send SIGTERM to stop the server
try {
process.kill(state.pid, "SIGTERM");
// Wait for process to stop
let attempts = 0;
const maxAttempts = 10;
while (isProcessRunning(state.pid) && attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 500));
attempts++;
}
if (isProcessRunning(state.pid)) {
if (argv.force) {
process.kill(state.pid, "SIGKILL");
if (spinner) {
spinner.succeed(chalk.green("Server force stopped"));
}
}
else {
if (spinner) {
spinner.warn(chalk.yellow("Server not responding, use --force to kill"));
}
return;
}
}
else {
if (spinner) {
spinner.succeed(chalk.green("Server stopped successfully"));
}
}
}
catch (killError) {
if (spinner) {
spinner.fail(chalk.red("Failed to stop server"));
}
logger.error(chalk.red(`Error: ${killError instanceof Error ? killError.message : String(killError)}`));
return;
}
clearServerState();
logger.always(chalk.green(`Stopped server (PID: ${state.pid})`));
}
catch (error) {
if (spinner) {
spinner.fail(chalk.red("Failed to stop server"));
}
logger.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
static async executeStatus(argv) {
try {
const state = loadServerState();
const status = {
running: false,
pid: null,
port: null,
host: null,
framework: null,
basePath: null,
uptime: null,
startTime: 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();
}
if (argv.format === "json") {
logger.always(JSON.stringify(status, null, 2));
return;
}
// Text format
logger.always(chalk.bold("\nNeuroLink Server Status:"));
logger.always("");
if (status.running) {
logger.always(` Status: ${chalk.green("RUNNING")}`);
logger.always(` PID: ${chalk.cyan(status.pid)}`);
logger.always(` URL: ${chalk.cyan(`http://${status.host === "0.0.0.0" ? "localhost" : status.host}:${status.port}`)}`);
logger.always(` Framework: ${chalk.cyan(status.framework)}`);
logger.always(` Base Path: ${chalk.cyan(status.basePath)}`);
logger.always(` Started: ${chalk.cyan(status.startTime)}`);
logger.always(` Uptime: ${chalk.cyan(formatUptime(status.uptime ?? 0))}`);
}
else {
logger.always(` Status: ${chalk.yellow("NOT RUNNING")}`);
logger.always("");
logger.always(chalk.gray("Use 'neurolink server start' to start the server"));
}
logger.always("");
}
catch (error) {
logger.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
static async executeOpenAPI(argv) {
const spinner = argv.quiet
? null
: ora("Generating OpenAPI specification...").start();
try {
// Dynamically import OpenAPI generator
const { OpenAPIGenerator } = await import("../../lib/server/openapi/index.js");
const generator = new OpenAPIGenerator({
basePath: argv.basePath ?? "/api",
info: {
title: argv.title,
version: argv.version,
},
});
const spec = generator.generate();
let output;
if (argv.format === "yaml") {
// Simple YAML conversion (for production, use a proper YAML library)
output = generator.toYAML();
}
else {
output = JSON.stringify(spec, null, 2);
}
if (argv.output) {
// Ensure directory exists
const dir = path.dirname(argv.output);
if (dir && dir !== "." && !fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(argv.output, output);
if (spinner) {
spinner.succeed(chalk.green(`OpenAPI spec saved to ${argv.output}`));
}
logger.always(chalk.bold("\nGenerated OpenAPI Specification:"));
logger.always(` File: ${chalk.cyan(argv.output)}`);
logger.always(` Format: ${chalk.cyan(argv.format ?? "json")}`);
logger.always(` Endpoints: ${chalk.cyan(Object.keys(spec.paths).length)}`);
logger.always(` Schemas: ${chalk.cyan(Object.keys(spec.components.schemas).length)}`);
}
else {
if (spinner) {
spinner.stop();
}
logger.always(output);
}
}
catch (error) {
if (spinner) {
spinner.fail(chalk.red("Failed to generate OpenAPI spec"));
}
logger.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
static async executeRoutes(argv) {
try {
// Dynamically import route definitions and types
const { createAllRoutes } = await import("../../lib/server/routes/index.js");
const routeGroups = createAllRoutes(argv.basePath ?? "/api");
// Flatten route groups into individual routes with group info
let flatRoutes = [];
for (const group of routeGroups) {
// Extract group name from prefix (e.g., "/api/agent" -> "agent")
const groupName = group.prefix.split("/").filter(Boolean).pop() || "root";
for (const route of group.routes) {
flatRoutes.push({
method: route.method,
path: route.path,
description: route.description,
group: groupName,
});
}
}
// Filter routes by group and method
if (argv.group && argv.group !== "all") {
flatRoutes = flatRoutes.filter((r) => r.group === argv.group);
}
if (argv.method && argv.method !== "all") {
flatRoutes = flatRoutes.filter((r) => r.method === argv.method);
}
// Output based on format
const format = argv.format;
if (format === "json") {
logger.always(JSON.stringify(flatRoutes, null, 2));
}
else if (format === "table") {
// Table format output
logger.always(chalk.bold("\nRegistered Routes:"));
logger.always("");
const methodColors = {
GET: chalk.green,
POST: chalk.blue,
PUT: chalk.yellow,
DELETE: chalk.red,
PATCH: chalk.magenta,
};
for (const route of flatRoutes) {
const colorFn = methodColors[route.method] || chalk.white;
logger.always(` ${colorFn(route.method.padEnd(7))} ${route.path.padEnd(40)} ${chalk.gray(route.description || "")}`);
}
logger.always("");
logger.always(chalk.gray(`Total: ${flatRoutes.length} routes`));
}
else {
// Text format
for (const route of flatRoutes) {
logger.always(`${route.method} ${route.path}`);
}
}
}
catch (error) {
logger.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
static async executeConfig(argv) {
try {
// Handle reset
if (argv.reset) {
try {
if (fs.existsSync(CONFIG_FILE)) {
fs.unlinkSync(CONFIG_FILE);
}
logger.always(chalk.green("Configuration reset to defaults"));
return;
}
catch {
logger.error(chalk.red("Failed to reset configuration"));
process.exit(1);
}
}
const config = loadServerConfig();
// Handle --get
if (argv.get) {
const value = getNestedValue(config, argv.get);
if (value === undefined) {
logger.error(chalk.red(`Unknown config key: ${argv.get}`));
process.exit(1);
}
if (argv.format === "json") {
logger.always(JSON.stringify({ [argv.get]: value }, null, 2));
}
else {
logger.always(`${argv.get}: ${JSON.stringify(value)}`);
}
return;
}
// Handle --set
if (argv.set) {
const [key, ...valueParts] = argv.set.split("=");
const value = valueParts.join("=");
if (!key || value === undefined || value === "") {
logger.error(chalk.red("Invalid format. Use: key=value"));
process.exit(1);
}
setNestedValue(config, key, parseConfigValue(value));
saveServerConfig(config);
logger.always(chalk.green(`Set ${key} = ${value}`));
return;
}
// Show all config
if (argv.format === "json") {
logger.always(JSON.stringify(config, null, 2));
}
else {
logger.always(chalk.bold("\nServer Configuration:"));
logger.always("");
logger.always(` ${chalk.cyan("Port:")} ${config.defaultPort}`);
logger.always(` ${chalk.cyan("Host:")} ${config.defaultHost}`);
logger.always(` ${chalk.cyan("Framework:")} ${config.defaultFramework}`);
logger.always(` ${chalk.cyan("Base Path:")} ${config.defaultBasePath}`);
logger.always("");
logger.always(` ${chalk.cyan("CORS:")} ${config.cors.enabled ? "enabled" : "disabled"}`);
logger.always(` ${chalk.cyan("Rate Limit:")} ${config.rateLimit.enabled ? "enabled" : "disabled"}`);
logger.always(` ${chalk.cyan("Swagger:")} ${config.swagger.enabled ? "enabled" : "disabled"}`);
logger.always("");
logger.always(chalk.gray(`Config file: ${CONFIG_FILE}`));
}
}
catch (error) {
logger.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
}
//# sourceMappingURL=server.js.map