import type { ChannelMeta } from "./plugins/types.js"; import type { ChannelId } from "./plugins/types.js"; import { requireActivePluginRegistry } from "../plugins/runtime.js"; // Channel docking: add new core channels here (order + meta + aliases), then // register the plugin in its extension entrypoint and keep protocol IDs in sync. export const CHAT_CHANNEL_ORDER = [ "telegram", "whatsapp", "discord", "slack", "signal", "imessage", ] as const; export type ChatChannelId = (typeof CHAT_CHANNEL_ORDER)[number]; export const CHANNEL_IDS = [...CHAT_CHANNEL_ORDER] as const; export const DEFAULT_CHAT_CHANNEL: ChatChannelId = "whatsapp"; export type ChatChannelMeta = ChannelMeta; const WEBSITE_URL = "https://clawd.bot"; const CHAT_CHANNEL_META: Record = { telegram: { id: "telegram", label: "Telegram", selectionLabel: "Telegram (Bot API)", detailLabel: "Telegram Bot", docsPath: "/channels/telegram", docsLabel: "telegram", blurb: "simplest way to get started — register a bot with @BotFather and get going.", systemImage: "paperplane", selectionDocsPrefix: "", selectionDocsOmitLabel: true, selectionExtras: [WEBSITE_URL], }, whatsapp: { id: "whatsapp", label: "WhatsApp", selectionLabel: "WhatsApp (QR link)", detailLabel: "WhatsApp Web", docsPath: "/channels/whatsapp", docsLabel: "whatsapp", blurb: "works with your own number; recommend a separate phone + eSIM.", systemImage: "message", }, discord: { id: "discord", label: "Discord", selectionLabel: "Discord (Bot API)", detailLabel: "Discord Bot", docsPath: "/channels/discord", docsLabel: "discord", blurb: "very well supported right now.", systemImage: "bubble.left.and.bubble.right", }, slack: { id: "slack", label: "Slack", selectionLabel: "Slack (Socket Mode)", detailLabel: "Slack Bot", docsPath: "/channels/slack", docsLabel: "slack", blurb: "supported (Socket Mode).", systemImage: "number", }, signal: { id: "signal", label: "Signal", selectionLabel: "Signal (signal-cli)", detailLabel: "Signal REST", docsPath: "/channels/signal", docsLabel: "signal", blurb: 'signal-cli linked device; more setup (David Reagans: "Hop on Discord.").', systemImage: "antenna.radiowaves.left.and.right", }, imessage: { id: "imessage", label: "iMessage", selectionLabel: "iMessage (imsg)", detailLabel: "iMessage", docsPath: "/channels/imessage", docsLabel: "imessage", blurb: "this is still a work in progress.", systemImage: "message.fill", }, }; export const CHAT_CHANNEL_ALIASES: Record = { imsg: "imessage", }; const normalizeChannelKey = (raw?: string | null): string | undefined => { const normalized = raw?.trim().toLowerCase(); return normalized || undefined; }; export function listChatChannels(): ChatChannelMeta[] { return CHAT_CHANNEL_ORDER.map((id) => CHAT_CHANNEL_META[id]); } export function listChatChannelAliases(): string[] { return Object.keys(CHAT_CHANNEL_ALIASES); } export function getChatChannelMeta(id: ChatChannelId): ChatChannelMeta { return CHAT_CHANNEL_META[id]; } export function normalizeChatChannelId(raw?: string | null): ChatChannelId | null { const normalized = normalizeChannelKey(raw); if (!normalized) return null; const resolved = CHAT_CHANNEL_ALIASES[normalized] ?? normalized; return CHAT_CHANNEL_ORDER.includes(resolved as ChatChannelId) ? (resolved as ChatChannelId) : null; } // Channel docking: prefer this helper in shared code. Importing from // `src/channels/plugins/*` can eagerly load channel implementations. export function normalizeChannelId(raw?: string | null): ChatChannelId | null { return normalizeChatChannelId(raw); } // Normalizes registered channel plugins (bundled or external). // // Keep this light: we do not import channel plugins here (those are "heavy" and can pull in // monitors, web login, etc). The plugin registry must be initialized first. export function normalizeAnyChannelId(raw?: string | null): ChannelId | null { const key = normalizeChannelKey(raw); if (!key) return null; const registry = requireActivePluginRegistry(); const hit = registry.channels.find((entry) => { const id = String(entry.plugin.id ?? "") .trim() .toLowerCase(); if (id && id === key) return true; return (entry.plugin.meta.aliases ?? []).some((alias) => alias.trim().toLowerCase() === key); }); return (hit?.plugin.id as ChannelId | undefined) ?? null; } export function formatChannelPrimerLine(meta: ChatChannelMeta): string { return `${meta.label}: ${meta.blurb}`; } export function formatChannelSelectionLine( meta: ChatChannelMeta, docsLink: (path: string, label?: string) => string, ): string { const docsPrefix = meta.selectionDocsPrefix ?? "Docs:"; const docsLabel = meta.docsLabel ?? meta.id; const docs = meta.selectionDocsOmitLabel ? docsLink(meta.docsPath) : docsLink(meta.docsPath, docsLabel); const extras = (meta.selectionExtras ?? []).filter(Boolean).join(" "); return `${meta.label} — ${meta.blurb} ${docsPrefix ? `${docsPrefix} ` : ""}${docs}${extras ? ` ${extras}` : ""}`; }