/** * v2 Channel Adapter interface. * * Channel adapters bridge NanoClaw with messaging platforms (Discord, Slack, etc.). * Two patterns: native adapters (implement directly) or Chat SDK bridge (wrap a Chat SDK adapter). */ /** Passed to the adapter at setup time. */ export interface ChannelSetup { /** Called when an inbound message arrives from the platform. */ onInbound(platformId: string, threadId: string | null, message: InboundMessage): void | Promise; /** * Called by admin-transport adapters (CLI) that want to route a message to * an arbitrary channel/platform and optionally redirect replies elsewhere. * Regular chat adapters should use `onInbound`; `onInboundEvent` skips the * adapter-channel-type injection so the caller can target any wired mg. */ onInboundEvent(event: InboundEvent): void | Promise; /** Called when the adapter discovers metadata about a conversation. */ onMetadata(platformId: string, name?: string, isGroup?: boolean): void; /** Called when a user clicks a button/action in a card (e.g., ask_user_question response). */ onAction(questionId: string, selectedOption: string, userId: string): void; } /** Delivery address used for reply-to overrides and (normally) the inbound's own origin. */ export interface DeliveryAddress { channelType: string; platformId: string; threadId: string | null; } /** * Full inbound event handed to the router. * * `channelType` + `platformId` + `threadId` identify which messaging group / * session receives the message. `replyTo`, when set, overrides where the * agent's reply is delivered — used by the CLI admin transport when the * operator wants a message routed to one channel but replies echoed back to * their terminal. Agents cannot set `replyTo`; it is a router-layer concept * set only by external adapters carrying operator intent. */ export interface InboundEvent { channelType: string; platformId: string; threadId: string | null; message: { id: string; kind: 'chat' | 'chat-sdk'; content: string; // JSON blob timestamp: string; /** * Platform-confirmed bot-mention signal forwarded from the adapter. * See InboundMessage.isMention for the full explanation. */ isMention?: boolean; /** True when the source is a group/channel thread, false for DMs. */ isGroup?: boolean; }; replyTo?: DeliveryAddress; } /** Inbound message from adapter to host. */ export interface InboundMessage { id: string; kind: 'chat' | 'chat-sdk'; content: unknown; // JS object — host will JSON.stringify before writing to session DB timestamp: string; /** * Platform-confirmed signal that this message is a mention of the bot. * * Set by adapters that know the platform's own mention semantics — e.g. * the Chat SDK bridge sets it true from `onNewMention` / `onDirectMessage` * and forwards `message.isMention` from `onSubscribedMessage`. Use this * in the router instead of agent-name regex matching, which breaks on * platforms where the mention text is the bot's platform username (e.g. * Telegram's `@nanoclaw_v2_refactr_1_bot`) rather than the agent_group * display name (e.g. `@Andy`). * * Adapters that don't set it (native / legacy) leave it undefined — the * router falls back to text-match against agent_group_name. */ isMention?: boolean; /** True when the source is a group/channel thread, false for DMs. */ isGroup?: boolean; } /** A file attachment to deliver alongside a message. */ export interface OutboundFile { filename: string; data: Buffer; } /** Outbound message from host to adapter. */ export interface OutboundMessage { kind: string; content: unknown; // parsed JSON from messages_out files?: OutboundFile[]; // file attachments from the session outbox } /** Discovered conversation info (from syncConversations). */ export interface ConversationInfo { platformId: string; name: string; isGroup: boolean; } /** The v2 channel adapter contract. */ export interface ChannelAdapter { name: string; channelType: string; /** * Whether this adapter models conversations as threads. * * true — adapter's platform uses threads as the primary conversation unit * (Discord, Slack, Linear, GitHub). One thread = one session; the * agent replies into the originating thread. * false — adapter's platform treats the channel itself as the conversation * (Telegram, WhatsApp, iMessage). Thread ids are stripped at the * router; agent replies go to the channel. */ supportsThreads: boolean; // Lifecycle setup(config: ChannelSetup): Promise; teardown(): Promise; isConnected(): boolean; // Outbound delivery — returns the platform message ID if available deliver(platformId: string, threadId: string | null, message: OutboundMessage): Promise; // Optional setTyping?(platformId: string, threadId: string | null): Promise; syncConversations?(): Promise; resolveChannelName?(platformId: string): Promise; /** * Subscribe the bot to a thread so follow-up messages route via the * platform's "subscribed message" path (onSubscribedMessage in Chat SDK). * Called by the router when a mention-sticky wiring first engages in a * thread. Idempotent: calling twice on the same thread is a no-op. * * Platforms without a subscription concept can omit this; the router * treats absence as a no-op. */ subscribe?(platformId: string, threadId: string): Promise; /** * Open (or fetch) a DM with this user, returning the platform_id of the * resulting DM channel. Called by the host on demand to initiate cold * DMs — approvals, pairing handshakes, host-initiated notifications — to * users who may never have messaged the bot themselves. * * Omit this method on channels where the user handle IS already the DM * chat id (Telegram, WhatsApp, iMessage, email, Matrix). Callers will * fall through to using the handle directly. * * For channels that distinguish user id from DM channel id (Discord, * Slack, Teams, Webex, gChat): implement by delegating to Chat SDK's * chat.openDM, which hits the platform's idempotent open-DM endpoint. * Returning the same platform_id on repeated calls is expected. */ openDM?(userHandle: string): Promise; } /** Factory function that creates a channel adapter (returns null if credentials missing). */ export type ChannelAdapterFactory = () => ChannelAdapter | Promise | null; /** Registration entry for a channel adapter. */ export interface ChannelRegistration { factory: ChannelAdapterFactory; containerConfig?: { mounts?: Array<{ hostPath: string; containerPath: string; readonly: boolean }>; env?: Record; }; }