From 701fcfcaed32f34b835aa53b3167b30709286d54 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 9 Feb 2026 20:22:34 -0800 Subject: [PATCH] refactor(01-03): extract services/skills/gateway/mcp into services.ts, reduce index.ts to 140 lines - Extract initSkills(), initMcp(), loadSystemPrompt(), initPairingManager(), createGateway(), startServices() into services.ts - daemon/index.ts reduced from 386 to 140 lines (64% reduction, 87% from 1087 baseline) - Organize imports with section comments (External, Config, Daemon Modules, Infrastructure) - Add section dividers in startDaemon() (Data & Sessions, Core Services, Model & Prompt, Gateway & Channels, Tier 1 Tools, Lifecycle) - Convert unused value imports to type-only imports - DaemonContext interface and re-exports unchanged --- src/daemon/index.ts | 359 +++++++---------------------------------- src/daemon/services.ts | 269 ++++++++++++++++++++++++++++++ 2 files changed, 326 insertions(+), 302 deletions(-) create mode 100644 src/daemon/services.ts diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 445201b..85d9479 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -1,32 +1,35 @@ -import { Lifecycle } from './lifecycle.js'; -import { createModelRouter } from './models.js'; -import { initMemory } from './memory.js'; -import { createMessageRouter } from './routing.js'; -import { initAgents } from './agents.js'; -import { initTools } from './tools.js'; -import { registerChannels } from './channels.js'; -import type { Config } from '../config/index.js'; -import type { AudioTranscriptionConfig } from '../models/media.js'; -import { ModelRouter } from '../models/index.js'; -import { AgentOrchestrator } from '../backends/index.js'; -import { OutboundAttachmentCollector } from '../backends/native/attachments.js'; -import { SessionStore, SessionManager, parseDuration } from '../session/index.js'; -import { HookEngine } from '../hooks/index.js'; -import type { ToolRegistry, ToolExecutor, BrowserManager } from '../tools/index.js'; -import { createSessionTools, createAgentsListTool, createMessageSendTool, createCronTools } from '../tools/index.js'; -import { GatewayServer } from '../gateway/index.js'; -import { ChannelRegistry, PairingManager } from '../channels/index.js'; -import type { CronScheduler } from '../automation/index.js'; -import { HeartbeatMonitor } from '../automation/index.js'; -import { McpManager } from '../mcp/index.js'; -import { SkillRegistry, SkillInstaller, loadAllSkills } from '../skills/index.js'; -import { assembleSystemPrompt } from '../prompt/index.js'; -import type { AgentConfigRegistry, AgentRouter } from '../agents/index.js'; -import type { SandboxManager } from '../sandbox/index.js'; +// ── External ── import { resolve } from 'path'; import { homedir } from 'os'; import { mkdirSync } from 'fs'; +// ── Config & Types ── +import type { Config } from '../config/index.js'; +import type { AudioTranscriptionConfig } from '../models/media.js'; +import type { ToolRegistry, ToolExecutor, BrowserManager } from '../tools/index.js'; +import type { AgentConfigRegistry, AgentRouter } from '../agents/index.js'; +import type { SandboxManager } from '../sandbox/index.js'; + +// ── Daemon Modules ── +import { Lifecycle } from './lifecycle.js'; +import { createModelRouter } from './models.js'; +import { initMemory } from './memory.js'; +import { initTools } from './tools.js'; +import { initAgents } from './agents.js'; +import { createMessageRouter } from './routing.js'; +import { registerChannels } from './channels.js'; +import { initSkills, initMcp, loadSystemPrompt, initPairingManager, createGateway, startServices } from './services.js'; + +// ── Infrastructure ── +import type { ModelRouter } from '../models/index.js'; +import { SessionStore, SessionManager, parseDuration } from '../session/index.js'; +import { HookEngine } from '../hooks/index.js'; +import { createSessionTools, createAgentsListTool, createMessageSendTool, createCronTools } from '../tools/index.js'; +import { ChannelRegistry } from '../channels/index.js'; +import type { McpManager } from '../mcp/index.js'; +import type { SkillRegistry, SkillInstaller } from '../skills/index.js'; +import type { GatewayServer } from '../gateway/index.js'; + export interface DaemonContext { config: Config; lifecycle: Lifecycle; @@ -47,33 +50,13 @@ export interface DaemonContext { browserManager?: BrowserManager; } -function loadSystemPrompt(config: Config): string { - const searchDirs = [ - process.cwd(), - resolve(import.meta.dirname, '../..'), - ...(config.prompt.search_dirs ?? []), - ]; - - const result = assembleSystemPrompt({ - searchDirs, - extraSections: config.prompt.extra_sections, - }); - - if (result.loadedFiles.length > 0) { - console.log(`Loaded prompt templates: ${result.loadedFiles.map(f => f.split('/').pop()).join(', ')}`); - } - - return result.prompt; -} - export async function startDaemon(config: Config): Promise { const lifecycle = new Lifecycle(); - // Ensure data directory exists (FLYNN_DATA_DIR overrides default for Docker/custom deployments) + // ── Data & Sessions ── const dataDir = process.env.FLYNN_DATA_DIR ?? resolve(homedir(), '.local/share/flynn'); mkdirSync(dataDir, { recursive: true }); - // Initialize session store and manager const sessionStore = new SessionStore(resolve(dataDir, 'sessions.db')); const sessionManager = new SessionManager(sessionStore); @@ -82,304 +65,76 @@ export async function startDaemon(config: Config): Promise { console.log('Session store closed'); }); - // Session pruning timer (TTL-based cleanup) const ttlMs = parseDuration(config.sessions?.ttl ?? '30d'); if (ttlMs) { const pruneInterval = setInterval(() => { - const cutoff = Math.floor((Date.now() - ttlMs) / 1000); // created_at is unix seconds + const cutoff = Math.floor((Date.now() - ttlMs) / 1000); const pruned = sessionStore.pruneStale(cutoff); if (pruned.length > 0) { sessionManager.evictSessions(pruned); console.log(`Pruned ${pruned.length} stale session(s) (TTL: ${config.sessions?.ttl ?? '30d'})`); } - }, 3_600_000); // every hour - - lifecycle.onShutdown(async () => { - clearInterval(pruneInterval); - }); + }, 3_600_000); + lifecycle.onShutdown(async () => { clearInterval(pruneInterval); }); } - // Initialize hook engine + // ── Core Services ── const hookEngine = new HookEngine(config.hooks); - - // Initialize tool registry, executor, web search, process tools, browser tools, and tool policy const { toolRegistry, toolExecutor, browserManager } = initTools({ config, lifecycle, hookEngine }); - - // Initialize memory store, vector search, and memory tools - const { memoryStore, hybridSearch, memoryDir } = await initMemory({ config, dataDir, lifecycle, toolRegistry }); - - // Initialize MCP manager and start configured servers - const mcpManager = new McpManager(toolRegistry); - - if (config.mcp.servers.length > 0) { - console.log(`Starting ${config.mcp.servers.length} MCP server(s)...`); - await mcpManager.startAll(config.mcp.servers); - } - - lifecycle.onShutdown(async () => { - await mcpManager.stopAll(); - console.log('MCP servers stopped'); - }); - - // Initialize skills system - const defaultManagedDir = resolve(homedir(), '.flynn/workspace/skills'); - const skillRegistry = new SkillRegistry(); - const skillInstaller = new SkillInstaller(config.skills.managed_dir ?? defaultManagedDir); - - const skills = loadAllSkills({ - bundledDir: config.skills.bundled_dir, - managedDir: config.skills.managed_dir ?? defaultManagedDir, - workspaceDir: config.skills.workspace_dir, - }); - - for (const skill of skills) { - skillRegistry.register(skill); - } - - if (skills.length > 0) { - const available = skillRegistry.listAvailable().length; - console.log(`Loaded ${skills.length} skill(s) (${available} available)`); - } - - // Initialize agent config registry, router, and sandbox manager + const { memoryStore, memoryDir } = await initMemory({ config, dataDir, lifecycle, toolRegistry }); + const mcpManager = await initMcp(config, lifecycle, toolRegistry); + const { skillRegistry, skillInstaller } = initSkills(config); const { agentConfigRegistry, agentRouter, sandboxManager } = await initAgents({ config, lifecycle }); - // Initialize audio transcription config + // ── Model & Prompt ── const audioConfig: AudioTranscriptionConfig = { endpoint: config.audio.transcription_endpoint, apiKey: config.audio.transcription_api_key, model: config.audio.transcription_model, }; - - // Initialize model router const modelRouter = createModelRouter(config); + const systemPrompt = loadSystemPrompt(config, skillRegistry); - // Load system prompt and append skill instructions - let systemPrompt = loadSystemPrompt(config); - const skillAdditions = skillRegistry.getSystemPromptAdditions(); - if (skillAdditions) { - systemPrompt = `${systemPrompt}\n\n# Available Skills\n\n${skillAdditions}`; - } - - // Initialize channel registry (created early so the gateway can reference it) + // ── Gateway & Channels ── const channelRegistry = new ChannelRegistry(); + const pairingManager = initPairingManager(config); - // Create PairingManager if pairing is enabled - let pairingManager: PairingManager | undefined; - if (config.pairing.enabled) { - // Parse code_ttl: supports '5m', '1h', '30s' → milliseconds - const ttlMatch = config.pairing.code_ttl.match(/^(\d+)(s|m|h)$/); - const codeTtlMs = ttlMatch - ? Number(ttlMatch[1]) * ({ s: 1000, m: 60_000, h: 3_600_000 }[ttlMatch[2] as 's' | 'm' | 'h']) - : 5 * 60_000; // default 5 minutes + let channelAgents: ReturnType['agents'] | null = null; - pairingManager = new PairingManager({ - enabled: true, - codeTtl: codeTtlMs, - codeLength: config.pairing.code_length, - }); - console.log(`Pairing codes enabled (TTL: ${config.pairing.code_ttl}, length: ${config.pairing.code_length})`); - } - - // Mutable reference to channel agents map — set after createMessageRouter() below. - // This allows the gateway's getTokenUsage callback to access channel agent usage data. - let channelAgents: Map | null = null; - - // Initialize gateway WebSocket server - const gateway = new GatewayServer({ - port: config.server.port, - host: config.server.localhost ? '127.0.0.1' : '0.0.0.0', - sessionManager, - modelClient: modelRouter, - systemPrompt, - toolRegistry, - toolExecutor, - auth: { - token: config.server.token, - tailscaleIdentity: config.server.tailscale_identity, - }, - authHttp: config.server.auth_http, - lock: config.server.lock, - uiDir: resolve(import.meta.dirname, '../gateway/ui'), - config, - channelRegistry, - pairingManager, - restart: async () => { - console.log('Restart requested via gateway'); - await lifecycle.shutdown(); - // Exit with code 75 (EX_TEMPFAIL) — process supervisor should restart - process.exit(75); - }, - getTokenUsage: () => { - const results: Array<{ - sessionId: string; - primary: { inputTokens: number; outputTokens: number; calls: number }; - delegation: Record; - total: { inputTokens: number; outputTokens: number; calls: number; estimatedCost: number }; - }> = []; - - // Collect usage from gateway WebSocket sessions (NativeAgent-based) - const sessionBridge = gateway.getSessionBridge(); - for (const entry of sessionBridge.getAllUsage()) { - results.push(entry); - } - - // Collect usage from channel agents (AgentOrchestrator-based, has full delegation data) - if (channelAgents) { - for (const [sessionId, { orchestrator }] of channelAgents) { - const usage = orchestrator.getUsage(); - results.push({ - sessionId, - primary: usage.primary, - delegation: usage.delegation, - total: usage.total, - }); - } - } - - return results; - }, + const gateway = createGateway({ + config, sessionManager, modelRouter, systemPrompt, toolRegistry, toolExecutor, + channelRegistry, pairingManager, lifecycle, + getChannelAgents: () => channelAgents, }); - if (config.server.token) { - console.log(`Gateway auth: token required${config.server.tailscale_identity ? ' + Tailscale identity' : ''}`); - } - - // ── Channel Registry ────────────────────────────────────────── - - // Set up the unified message handler const messageRouter = createMessageRouter({ - sessionManager, - modelRouter, - systemPrompt, - toolRegistry, - toolExecutor, - config, - memoryStore, - agentConfigRegistry, - agentRouter, - sandboxManager, - audioConfig, + sessionManager, modelRouter, systemPrompt, toolRegistry, toolExecutor, + config, memoryStore, agentConfigRegistry, agentRouter, sandboxManager, audioConfig, }); channelRegistry.setMessageHandler(messageRouter.handler); - - // Wire channel agents into the getTokenUsage callback (late binding) channelAgents = messageRouter.agents; - // Register all channel adapters (Telegram, Discord, Slack, WhatsApp, WebChat, cron, webhooks, Gmail) const { cronScheduler } = registerChannels({ config, channelRegistry, hookEngine, pairingManager, gateway }); - // ── Register Tier 1 agent tools ───────────────────────────── - - // Session management tools (list, history, create, delete) - for (const tool of createSessionTools(sessionManager)) { - toolRegistry.register(tool); - } - - // Agent discovery tool + // ── Tier 1 Tools ── + for (const tool of createSessionTools(sessionManager)) { toolRegistry.register(tool); } toolRegistry.register(createAgentsListTool(agentConfigRegistry)); - - // Cross-channel messaging tool toolRegistry.register(createMessageSendTool(channelRegistry)); - - // Cron management tools (if scheduler is active) if (cronScheduler) { - for (const tool of createCronTools(cronScheduler)) { - toolRegistry.register(tool); - } + for (const tool of createCronTools(cronScheduler)) { toolRegistry.register(tool); } } - // ── Signal Handlers ─────────────────────────────────────────── - - const signalHandler = () => { - lifecycle.shutdown().then(() => process.exit(0)); - }; - - process.on('SIGINT', signalHandler); - process.on('SIGTERM', signalHandler); - - lifecycle.onShutdown(async () => { - process.off('SIGINT', signalHandler); - process.off('SIGTERM', signalHandler); - }); - - // ── Start Services ──────────────────────────────────────────── - - // Register shutdown handler for channels (stops Telegram bot etc.) - lifecycle.onShutdown(async () => { - await channelRegistry.stopAll(); - console.log('Channel adapters stopped'); - }); - - // Start all channel adapters (Telegram long polling, WebChat status) - await channelRegistry.startAll(); - - // Start gateway (HTTP + WS server) - lifecycle.onShutdown(async () => { - await gateway.stop(); - console.log('Gateway server stopped'); - }); - - await gateway.start(); - - // ── Tailscale Serve ──────────────────────────────────────────── - if (config.server.tailscale?.serve) { - const { startTailscaleServe, stopTailscaleServe } = await import('../gateway/tailscale.js'); - const tsConfig = { - localPort: config.server.port, - servePort: config.server.tailscale.port, - hostname: config.server.tailscale.hostname, - }; - try { - await startTailscaleServe(tsConfig); - lifecycle.onShutdown(async () => { - await stopTailscaleServe(tsConfig); - }); - } catch { - console.warn('Tailscale Serve failed to start — gateway still accessible on local port'); - } - } - - // ── Heartbeat Monitor ────────────────────────────────────────── - const heartbeatMonitor = new HeartbeatMonitor({ - config: config.automation.heartbeat, - getGatewayPort: () => config.server.port, - modelRouter, - channelLister: channelRegistry, - memoryDir: config.memory.enabled ? memoryDir : undefined, - dataDir, - channelLookup: channelRegistry, - }); - - heartbeatMonitor.start(); - - lifecycle.onShutdown(async () => { - heartbeatMonitor.stop(); - console.log('Heartbeat monitor stopped'); - }); - - console.log('Flynn daemon started'); + // ── Lifecycle ── + await startServices({ config, lifecycle, channelRegistry, gateway, modelRouter, memoryDir, dataDir }); return { - config, - lifecycle, - sessionStore, - sessionManager, - hookEngine, - modelRouter, - toolRegistry, - toolExecutor, - gateway, - channelRegistry, - mcpManager, - skillRegistry, - skillInstaller, - agentConfigRegistry, - agentRouter, - sandboxManager, - browserManager, + config, lifecycle, sessionStore, sessionManager, hookEngine, modelRouter, + toolRegistry, toolExecutor, gateway, channelRegistry, mcpManager, + skillRegistry, skillInstaller, agentConfigRegistry, agentRouter, + sandboxManager, browserManager, }; } +// ── Re-exports for backward compatibility ── export { Lifecycle } from './lifecycle.js'; export { createClientFromConfig, anthropicToGitHubModel, createAutoFallbackClient, createModelRouter } from './models.js'; diff --git a/src/daemon/services.ts b/src/daemon/services.ts new file mode 100644 index 0000000..3e6fe2b --- /dev/null +++ b/src/daemon/services.ts @@ -0,0 +1,269 @@ +import type { Config } from '../config/index.js'; +import type { Lifecycle } from './lifecycle.js'; +import type { ToolRegistry, ToolExecutor } from '../tools/index.js'; +import type { AgentOrchestrator } from '../backends/index.js'; +import type { OutboundAttachmentCollector } from '../backends/native/attachments.js'; +import { ModelRouter } from '../models/index.js'; +import { SessionManager } from '../session/index.js'; +import { GatewayServer } from '../gateway/index.js'; +import { ChannelRegistry, PairingManager } from '../channels/index.js'; +import { HeartbeatMonitor } from '../automation/index.js'; +import { McpManager } from '../mcp/index.js'; +import { SkillRegistry, SkillInstaller, loadAllSkills } from '../skills/index.js'; +import { assembleSystemPrompt } from '../prompt/index.js'; +import { resolve } from 'path'; +import { homedir } from 'os'; + +// ── Skills ────────────────────────────────────────────────────── + +export interface SkillsResult { + skillRegistry: SkillRegistry; + skillInstaller: SkillInstaller; +} + +export function initSkills(config: Config): SkillsResult { + const defaultManagedDir = resolve(homedir(), '.flynn/workspace/skills'); + const skillRegistry = new SkillRegistry(); + const skillInstaller = new SkillInstaller(config.skills.managed_dir ?? defaultManagedDir); + + const skills = loadAllSkills({ + bundledDir: config.skills.bundled_dir, + managedDir: config.skills.managed_dir ?? defaultManagedDir, + workspaceDir: config.skills.workspace_dir, + }); + + for (const skill of skills) { + skillRegistry.register(skill); + } + + if (skills.length > 0) { + const available = skillRegistry.listAvailable().length; + console.log(`Loaded ${skills.length} skill(s) (${available} available)`); + } + + return { skillRegistry, skillInstaller }; +} + +// ── MCP ───────────────────────────────────────────────────────── + +export async function initMcp(config: Config, lifecycle: Lifecycle, toolRegistry: ToolRegistry): Promise { + const mcpManager = new McpManager(toolRegistry); + + if (config.mcp.servers.length > 0) { + console.log(`Starting ${config.mcp.servers.length} MCP server(s)...`); + await mcpManager.startAll(config.mcp.servers); + } + + lifecycle.onShutdown(async () => { + await mcpManager.stopAll(); + console.log('MCP servers stopped'); + }); + + return mcpManager; +} + +// ── System Prompt ─────────────────────────────────────────────── + +export function loadSystemPrompt(config: Config, skillRegistry: SkillRegistry): string { + const searchDirs = [ + process.cwd(), + resolve(import.meta.dirname, '../..'), + ...(config.prompt.search_dirs ?? []), + ]; + + const result = assembleSystemPrompt({ + searchDirs, + extraSections: config.prompt.extra_sections, + }); + + if (result.loadedFiles.length > 0) { + console.log(`Loaded prompt templates: ${result.loadedFiles.map(f => f.split('/').pop()).join(', ')}`); + } + + let prompt = result.prompt; + const skillAdditions = skillRegistry.getSystemPromptAdditions(); + if (skillAdditions) { + prompt = `${prompt}\n\n# Available Skills\n\n${skillAdditions}`; + } + + return prompt; +} + +// ── Pairing Manager ───────────────────────────────────────────── + +export function initPairingManager(config: Config): PairingManager | undefined { + if (!config.pairing.enabled) return undefined; + + const ttlMatch = config.pairing.code_ttl.match(/^(\d+)(s|m|h)$/); + const codeTtlMs = ttlMatch + ? Number(ttlMatch[1]) * ({ s: 1000, m: 60_000, h: 3_600_000 }[ttlMatch[2] as 's' | 'm' | 'h']) + : 5 * 60_000; + + const manager = new PairingManager({ + enabled: true, + codeTtl: codeTtlMs, + codeLength: config.pairing.code_length, + }); + console.log(`Pairing codes enabled (TTL: ${config.pairing.code_ttl}, length: ${config.pairing.code_length})`); + return manager; +} + +// ── Gateway ───────────────────────────────────────────────────── + +export interface GatewayDeps { + config: Config; + sessionManager: SessionManager; + modelRouter: ModelRouter; + systemPrompt: string; + toolRegistry: ToolRegistry; + toolExecutor: ToolExecutor; + channelRegistry: ChannelRegistry; + pairingManager?: PairingManager; + lifecycle: Lifecycle; + getChannelAgents: () => Map | null; +} + +export function createGateway(deps: GatewayDeps): GatewayServer { + const { config, sessionManager, modelRouter, systemPrompt, toolRegistry, toolExecutor, channelRegistry, pairingManager, lifecycle, getChannelAgents } = deps; + + const gateway = new GatewayServer({ + port: config.server.port, + host: config.server.localhost ? '127.0.0.1' : '0.0.0.0', + sessionManager, + modelClient: modelRouter, + systemPrompt, + toolRegistry, + toolExecutor, + auth: { + token: config.server.token, + tailscaleIdentity: config.server.tailscale_identity, + }, + authHttp: config.server.auth_http, + lock: config.server.lock, + uiDir: resolve(import.meta.dirname, '../gateway/ui'), + config, + channelRegistry, + pairingManager, + restart: async () => { + console.log('Restart requested via gateway'); + await lifecycle.shutdown(); + process.exit(75); + }, + getTokenUsage: () => { + const results: Array<{ + sessionId: string; + primary: { inputTokens: number; outputTokens: number; calls: number }; + delegation: Record; + total: { inputTokens: number; outputTokens: number; calls: number; estimatedCost: number }; + }> = []; + + const sessionBridge = gateway.getSessionBridge(); + for (const entry of sessionBridge.getAllUsage()) { + results.push(entry); + } + + const channelAgents = getChannelAgents(); + if (channelAgents) { + for (const [sessionId, { orchestrator }] of channelAgents) { + const usage = orchestrator.getUsage(); + results.push({ + sessionId, + primary: usage.primary, + delegation: usage.delegation, + total: usage.total, + }); + } + } + + return results; + }, + }); + + if (config.server.token) { + console.log(`Gateway auth: token required${config.server.tailscale_identity ? ' + Tailscale identity' : ''}`); + } + + return gateway; +} + +// ── Service Startup ───────────────────────────────────────────── + +export async function startServices(deps: { + config: Config; + lifecycle: Lifecycle; + channelRegistry: ChannelRegistry; + gateway: GatewayServer; + modelRouter: ModelRouter; + memoryDir: string; + dataDir: string; +}): Promise { + const { config, lifecycle, channelRegistry, gateway, modelRouter, memoryDir, dataDir } = deps; + + // Register shutdown handler for channels + lifecycle.onShutdown(async () => { + await channelRegistry.stopAll(); + console.log('Channel adapters stopped'); + }); + + // Start all channel adapters + await channelRegistry.startAll(); + + // Start gateway (HTTP + WS server) + lifecycle.onShutdown(async () => { + await gateway.stop(); + console.log('Gateway server stopped'); + }); + + await gateway.start(); + + // Tailscale Serve + if (config.server.tailscale?.serve) { + const { startTailscaleServe, stopTailscaleServe } = await import('../gateway/tailscale.js'); + const tsConfig = { + localPort: config.server.port, + servePort: config.server.tailscale.port, + hostname: config.server.tailscale.hostname, + }; + try { + await startTailscaleServe(tsConfig); + lifecycle.onShutdown(async () => { + await stopTailscaleServe(tsConfig); + }); + } catch { + console.warn('Tailscale Serve failed to start — gateway still accessible on local port'); + } + } + + // Heartbeat Monitor + const heartbeatMonitor = new HeartbeatMonitor({ + config: config.automation.heartbeat, + getGatewayPort: () => config.server.port, + modelRouter, + channelLister: channelRegistry, + memoryDir: config.memory.enabled ? memoryDir : undefined, + dataDir, + channelLookup: channelRegistry, + }); + + heartbeatMonitor.start(); + + lifecycle.onShutdown(async () => { + heartbeatMonitor.stop(); + console.log('Heartbeat monitor stopped'); + }); + + // Signal handlers + const signalHandler = () => { + lifecycle.shutdown().then(() => process.exit(0)); + }; + + process.on('SIGINT', signalHandler); + process.on('SIGTERM', signalHandler); + + lifecycle.onShutdown(async () => { + process.off('SIGINT', signalHandler); + process.off('SIGTERM', signalHandler); + }); + + console.log('Flynn daemon started'); +}