/** * Service discovery helpers for the Web UI dashboard. * * The gateway can surface a single "services" list so operators can see: * - which channel adapters are configured vs connected * - which automation subsystems are enabled (cron, webhooks, gmail, etc.) */ import type { ChannelRegistry } from '../../channels/index.js'; import type { Config } from '../../config/index.js'; export type ServiceType = 'channel' | 'automation' | 'tool'; // Channel adapters expose: disconnected|connecting|connected|error. // For config-driven subsystems we additionally use: // - configured: enabled in config, runtime status not tracked at this layer // - not_configured: disabled / missing config export type ServiceStatus = | 'disconnected' | 'connecting' | 'connected' | 'error' | 'configured' | 'not_configured'; export interface ServiceInfo { name: string; type: ServiceType; status: ServiceStatus; description: string; itemCount?: number; error?: string; metadata?: Record; } /** * Discover all services from config and channel registry. * * Returns configured channels and detects potential services from config * that may not yet be registered (e.g., gcal configured but no adapter created). */ export function discoverServices( config: Config, channelRegistry: ChannelRegistry, ): ServiceInfo[] { const services: ServiceInfo[] = []; const registeredChannels = channelRegistry.list(); const channelConfigs: Array<{ key: keyof Config; name: string; description: string }> = [ { key: 'telegram', name: 'telegram', description: 'Telegram bot' }, { key: 'discord', name: 'discord', description: 'Discord bot' }, { key: 'slack', name: 'slack', description: 'Slack app' }, { key: 'whatsapp', name: 'whatsapp', description: 'WhatsApp gateway' }, { key: 'matrix', name: 'matrix', description: 'Matrix bot' }, { key: 'signal', name: 'signal', description: 'Signal bot (signal-cli)' }, { key: 'mattermost', name: 'mattermost', description: 'Mattermost bot' }, { key: 'teams', name: 'teams', description: 'Microsoft Teams bot' }, { key: 'google_chat', name: 'google_chat', description: 'Google Chat bot' }, { key: 'bluebubbles', name: 'bluebubbles', description: 'iMessage via BlueBubbles' }, { key: 'line', name: 'line', description: 'LINE Messaging API bot' }, ]; for (const { key, name, description } of channelConfigs) { const configured = Boolean((config as unknown as Record)[String(key)]); const registered = registeredChannels.find(c => c.name === name); if (!configured) { services.push({ name, type: 'channel', status: 'not_configured', description }); continue; } if (registered) { services.push({ name, type: 'channel', status: registered.status as ServiceStatus, description }); continue; } // Config present but adapter not registered (or gateway running without it). services.push({ name, type: 'channel', status: 'disconnected', description }); } // Web search (tooling subsystem) services.push({ name: 'web_search', type: 'tool', status: 'configured', description: 'Web search provider', metadata: { provider: config.web_search?.provider ?? 'brave', endpoint: config.web_search?.endpoint, max_results: config.web_search?.max_results, }, }); // Audio transcription (tooling subsystem) const audioEnabled = Boolean(config.audio?.enabled); services.push({ name: 'audio_transcription', type: 'tool', status: audioEnabled ? 'configured' : 'not_configured', description: 'Audio transcription', metadata: { provider: config.audio?.provider?.type, endpoint: config.audio?.provider?.endpoint, model: config.audio?.provider?.model, }, }); // Docker sandboxing (tooling subsystem) services.push({ name: 'sandbox', type: 'tool', status: config.sandbox?.enabled ? 'configured' : 'not_configured', description: 'Docker sandbox for high-risk tool execution', metadata: { enabled: config.sandbox?.enabled ?? false, image: config.sandbox?.image, network: config.sandbox?.network, }, }); const automation = config.automation; const automationConfigs: Array<{ enabled: boolean; name: string; description: string; itemCount?: number }> = [ { enabled: automation.cron.length > 0, name: 'cron', description: 'Cron scheduler', itemCount: automation.cron.length }, { enabled: automation.webhooks.length > 0, name: 'webhooks', description: 'Webhook handler', itemCount: automation.webhooks.length }, { enabled: automation.gmail?.enabled ?? false, name: 'gmail', description: 'Gmail watcher' }, { enabled: automation.heartbeat?.enabled ?? false, name: 'heartbeat', description: 'Heartbeat monitor' }, { enabled: automation.gcal?.enabled ?? false, name: 'gcal', description: 'Google Calendar' }, { enabled: automation.gdocs?.enabled ?? false, name: 'gdocs', description: 'Google Docs' }, { enabled: automation.gdrive?.enabled ?? false, name: 'gdrive', description: 'Google Drive' }, { enabled: automation.gtasks?.enabled ?? false, name: 'gtasks', description: 'Google Tasks' }, ]; for (const auto of automationConfigs) { services.push({ name: auto.name, type: 'automation', status: auto.enabled ? 'configured' : 'not_configured', description: auto.description, ...(auto.itemCount !== undefined ? { itemCount: auto.itemCount } : {}), }); } services.push({ name: 'mcp', type: 'automation', status: config.mcp.servers.length > 0 ? 'configured' : 'not_configured', description: 'MCP servers', metadata: { serverCount: config.mcp.servers.length }, }); return services; }