/** * Node.js-compatible RCS server entry. * * Uses @hono/node-server + @hono/node-ws (available via acp-link workspace). * At build time, Vite bundles these into dist/cli-node.js so the published * npm package needs zero additional dependencies. * * Usage: * RCS_API_KEYS=sk-1234 slz rcs */ import { serve } from '@hono/node-server' import { serveStatic } from '@hono/node-server/serve-static' import { createNodeWebSocket } from '@hono/node-ws' import { Hono } from 'hono' import { cors } from 'hono/cors' import { logger } from 'hono/logger' import type { Context, Next } from 'hono' import { config } from './config.js' import { registerRoutes } from './registerRoutes.js' import { webCorsOptions } from './auth/cors.js' import { closeAllConnections } from './transport/ws-handler.js' import { closeAllAcpConnections } from './transport/acp-ws-handler.js' import { closeAllRelayConnections } from './transport/acp-relay-handler.js' import { startDisconnectMonitor } from './services/disconnect-monitor.js' export interface RcsServerOptions { apiKeys: string[] port?: number host?: string } export async function startRcsServer(opts: RcsServerOptions): Promise { // Set env vars so config module picks them up process.env.RCS_API_KEYS = opts.apiKeys.join(',') if (opts.port) process.env.RCS_PORT = String(opts.port) if (opts.host) process.env.RCS_HOST = opts.host const port = config.port const host = config.host const app = new Hono() // Create Node.js WebSocket adapter const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }) // Middleware app.use('*', logger()) app.use('*', async (c: Context, next: Next) => { const path = new URL(c.req.url).pathname if (path.includes('//')) { const normalized = path.replace(/\/+/g, '/') const url = new URL(c.req.url) url.pathname = normalized return app.fetch(new Request(url.toString(), c.req.raw)) } await next() }) app.use('/web/*', cors(webCorsOptions)) // Health check app.get('/health', (c: Context) => c.json({ status: 'ok', version: config.version }), ) // Register all routes with Node.js serveStatic and upgradeWebSocket registerRoutes(app, serveStatic as any, upgradeWebSocket) // Start HTTP server const server = serve({ fetch: app.fetch, port, hostname: host, }) // Inject WebSocket support for Node.js injectWebSocket(server) // Start disconnect monitor startDisconnectMonitor() console.log( `[RCS] Remote Control Server starting on ${host}:${port} (Node.js)`, ) console.log('[RCS] API key configuration loaded') console.log(`[RCS] Base URL: ${config.baseUrl || `http://localhost:${port}`}`) console.log(`[RCS] Disconnect timeout: ${config.disconnectTimeout}s`) // Browsers block 0.0.0.0 (Chrome Private Network Access); show localhost for clickable URL. const webUiHost = host === '0.0.0.0' || host === '::' ? 'localhost' : host console.log(`[RCS] Web UI: http://${webUiHost}:${port}/code/`) const workerBaseUrl = config.baseUrl || `http://${webUiHost}:${port}` console.log('[RCS] Worker connect:') console.log(`[RCS] export SALUZI_BRIDGE_BASE_URL=${workerBaseUrl}`) console.log('[RCS] export SALUZI_BRIDGE_OAUTH_TOKEN=') console.log('[RCS] slz') // Graceful shutdown function gracefulShutdown(signal: string) { console.log(`\n[RCS] Received ${signal}, shutting down...`) closeAllConnections() closeAllAcpConnections() closeAllRelayConnections() server.close() process.exit(0) } process.on('SIGINT', () => gracefulShutdown('SIGINT')) process.on('SIGTERM', () => gracefulShutdown('SIGTERM')) // Block forever — server keeps running await new Promise(() => {}) }