feat: complete DM pairing codes with channel adapters, gateway handlers, and TUI command (Tier 4 feature 4)

This commit is contained in:
William Valentin
2026-02-09 18:28:10 -08:00
parent 9d4d440ecf
commit 1e29da4da2
11 changed files with 270 additions and 7 deletions
+23 -1
View File
@@ -16,7 +16,7 @@ import { VectorStore, HybridSearch, createEmbeddingProvider, chunkText, contentH
import type { EmbeddingProvider as EmbeddingProviderInterface } from '../memory/index.js';
import { createMemoryTools } from '../tools/builtin/index.js';
import { GatewayServer } from '../gateway/index.js';
import { ChannelRegistry, TelegramAdapter, WebChatAdapter, DiscordAdapter, SlackAdapter, WhatsAppAdapter } from '../channels/index.js';
import { ChannelRegistry, TelegramAdapter, WebChatAdapter, DiscordAdapter, SlackAdapter, WhatsAppAdapter, PairingManager } from '../channels/index.js';
import { CronScheduler, WebhookHandler, HeartbeatMonitor, GmailWatcher } from '../automation/index.js';
import type { InboundMessage, OutboundMessage } from '../channels/index.js';
import { McpManager } from '../mcp/index.js';
@@ -793,6 +793,23 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
// Initialize channel registry (created early so the gateway can reference it)
const channelRegistry = new ChannelRegistry();
// 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
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<string, { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector }> | null = null;
@@ -815,6 +832,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
uiDir: resolve(import.meta.dirname, '../gateway/ui'),
config,
channelRegistry,
pairingManager,
restart: async () => {
console.log('Restart requested via gateway');
await lifecycle.shutdown();
@@ -883,6 +901,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
allowedChatIds: config.telegram.allowed_chat_ids,
requireMention: config.telegram.require_mention,
hookEngine,
pairingManager,
});
channelRegistry.register(telegramAdapter);
@@ -893,6 +912,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
allowedGuildIds: config.discord.allowed_guild_ids.length > 0 ? config.discord.allowed_guild_ids : undefined,
allowedChannelIds: config.discord.allowed_channel_ids.length > 0 ? config.discord.allowed_channel_ids : undefined,
requireMention: config.discord.require_mention,
pairingManager,
});
channelRegistry.register(discordAdapter);
}
@@ -905,6 +925,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
signingSecret: config.slack.signing_secret,
allowedChannelIds: config.slack.allowed_channel_ids.length > 0 ? config.slack.allowed_channel_ids : undefined,
requireMention: config.slack.require_mention,
pairingManager,
});
channelRegistry.register(slackAdapter);
}
@@ -916,6 +937,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
allowedGroupIds: config.whatsapp.allowed_group_ids.length > 0 ? config.whatsapp.allowed_group_ids : undefined,
requireMention: config.whatsapp.require_mention,
dataDir: config.whatsapp.data_dir,
pairingManager,
});
channelRegistry.register(whatsappAdapter);
}