@ebowwa/channel-types 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.ts ADDED
@@ -0,0 +1,429 @@
1
+ /**
2
+ * @ebowwa/channel-types/config
3
+ *
4
+ * Composable channel configuration loading.
5
+ * Supports Doppler (env vars), JSON files, and programmatic config.
6
+ *
7
+ * Usage:
8
+ * const config = loadChannelConfig(); // From env
9
+ * const config = parseChannelConfig(jsonString); // From JSON
10
+ * const config = createChannelConfig({ ... }); // Programmatic
11
+ */
12
+
13
+ import type { ChannelPlatform, ChannelId } from "./index.js";
14
+ import { createChannelId } from "./index.js";
15
+
16
+ // ============================================================
17
+ // CHANNEL CONFIG SCHEMA
18
+ // ============================================================
19
+
20
+ /** Base configuration for all channels */
21
+ export interface BaseChannelConfig {
22
+ /** Channel platform */
23
+ platform: ChannelPlatform;
24
+
25
+ /** Unique account/bot identifier */
26
+ accountId: string;
27
+
28
+ /** Enable/disable channel */
29
+ enabled: boolean;
30
+
31
+ /** Human-readable label */
32
+ label?: string;
33
+
34
+ /** Optional instance ID for multiple instances */
35
+ instanceId?: string;
36
+ }
37
+
38
+ /** Telegram channel configuration */
39
+ export interface TelegramChannelConfig extends BaseChannelConfig {
40
+ platform: "telegram";
41
+ /** Bot token from @BotFather */
42
+ botToken: string;
43
+ /** Polling interval in ms (default: 1000) */
44
+ pollingInterval?: number;
45
+ /** Allowed user IDs (empty = all) */
46
+ allowedUsers?: number[];
47
+ /** Allowed chat IDs (empty = all) */
48
+ allowedChats?: number[];
49
+ }
50
+
51
+ /** Discord channel configuration */
52
+ export interface DiscordChannelConfig extends BaseChannelConfig {
53
+ platform: "discord";
54
+ /** Bot token */
55
+ botToken: string;
56
+ /** Application ID */
57
+ applicationId?: string;
58
+ /** Guild ID to restrict to */
59
+ guildId?: string;
60
+ /** Channel IDs to listen to */
61
+ channelIds?: string[];
62
+ /** Enable slash commands */
63
+ enableSlashCommands?: boolean;
64
+ }
65
+
66
+ /** WhatsApp channel configuration */
67
+ export interface WhatsAppChannelConfig extends BaseChannelConfig {
68
+ platform: "whatsapp";
69
+ /** Webhook URL for business API */
70
+ webhookUrl?: string;
71
+ /** Phone number ID */
72
+ phoneNumberId?: string;
73
+ /** Access token */
74
+ accessToken?: string;
75
+ /** Or use QR code pairing (non-business) */
76
+ sessionPath?: string;
77
+ }
78
+
79
+ /** iMessage channel configuration */
80
+ export interface IMessageChannelConfig extends BaseChannelConfig {
81
+ platform: "imessage";
82
+ /** BlueBubbles server URL */
83
+ serverUrl?: string;
84
+ /** BlueBubbles password */
85
+ password?: string;
86
+ /** Or use local iMessage */
87
+ local?: boolean;
88
+ }
89
+
90
+ /** Slack channel configuration */
91
+ export interface SlackChannelConfig extends BaseChannelConfig {
92
+ platform: "slack";
93
+ /** Bot token (xoxb-...) */
94
+ botToken: string;
95
+ /** App token (xapp-...) for Socket Mode */
96
+ appToken?: string;
97
+ /** Signing secret */
98
+ signingSecret?: string;
99
+ /** Channel IDs to listen to */
100
+ channelIds?: string[];
101
+ }
102
+
103
+ /** Signal channel configuration */
104
+ export interface SignalChannelConfig extends BaseChannelConfig {
105
+ platform: "signal";
106
+ /** Phone number */
107
+ phoneNumber: string;
108
+ /** signal-cli path */
109
+ cliPath?: string;
110
+ /** Signal REST API URL */
111
+ apiUrl?: string;
112
+ }
113
+
114
+ /** CLI channel configuration (terminal-based) */
115
+ export interface CLIChannelConfig extends BaseChannelConfig {
116
+ platform: "cli";
117
+ /** Enable interactive mode */
118
+ interactive?: boolean;
119
+ /** Prompt string */
120
+ prompt?: string;
121
+ /** History file path */
122
+ historyPath?: string;
123
+ }
124
+
125
+ /** Web channel configuration */
126
+ export interface WebChannelConfig extends BaseChannelConfig {
127
+ platform: "web";
128
+ /** Port to listen on */
129
+ port?: number;
130
+ /** Host to bind to */
131
+ host?: string;
132
+ /** API key for authentication */
133
+ apiKey?: string;
134
+ /** Enable CORS */
135
+ cors?: boolean;
136
+ }
137
+
138
+ /** Union of all channel configs */
139
+ export type ChannelConfig =
140
+ | TelegramChannelConfig
141
+ | DiscordChannelConfig
142
+ | WhatsAppChannelConfig
143
+ | IMessageChannelConfig
144
+ | SlackChannelConfig
145
+ | SignalChannelConfig
146
+ | CLIChannelConfig
147
+ | WebChannelConfig;
148
+
149
+ /** Map of channel name -> config */
150
+ export type ChannelConfigMap = Record<string, ChannelConfig>;
151
+
152
+ // ============================================================
153
+ // CONFIG LOADING FROM DOPPLER (ENV VARS)
154
+ // ============================================================
155
+
156
+ /**
157
+ * Environment variable naming convention:
158
+ *
159
+ * {PLATFORM}_ENABLED - Enable channel (true/false)
160
+ * {PLATFORM}_BOT_TOKEN - Bot token
161
+ * {PLATFORM}_ACCOUNT_ID - Account identifier
162
+ * {PLATFORM}_{OPTION} - Platform-specific options
163
+ *
164
+ * Examples:
165
+ * TELEGRAM_ENABLED=true
166
+ * TELEGRAM_BOT_TOKEN=123456:ABC
167
+ * DISCORD_ENABLED=true
168
+ * DISCORD_BOT_TOKEN=xyz.abc
169
+ * DISCORD_GUILD_ID=123456789
170
+ */
171
+
172
+ /** Load all channel configs from environment variables (Doppler) */
173
+ export function loadChannelConfigsFromEnv(): ChannelConfigMap {
174
+ const configs: ChannelConfigMap = {};
175
+
176
+ // Telegram
177
+ if (isChannelEnabled("TELEGRAM")) {
178
+ configs.telegram = loadTelegramConfig();
179
+ }
180
+
181
+ // Discord
182
+ if (isChannelEnabled("DISCORD")) {
183
+ configs.discord = loadDiscordConfig();
184
+ }
185
+
186
+ // WhatsApp
187
+ if (isChannelEnabled("WHATSAPP")) {
188
+ configs.whatsapp = loadWhatsAppConfig();
189
+ }
190
+
191
+ // iMessage
192
+ if (isChannelEnabled("IMESSAGE")) {
193
+ configs.imessage = loadIMessageConfig();
194
+ }
195
+
196
+ // Slack
197
+ if (isChannelEnabled("SLACK")) {
198
+ configs.slack = loadSlackConfig();
199
+ }
200
+
201
+ // Signal
202
+ if (isChannelEnabled("SIGNAL")) {
203
+ configs.signal = loadSignalConfig();
204
+ }
205
+
206
+ // CLI (enabled by default)
207
+ if (isChannelEnabled("CLI", true)) {
208
+ configs.cli = loadCLIConfig();
209
+ }
210
+
211
+ // Web
212
+ if (isChannelEnabled("WEB")) {
213
+ configs.web = loadWebConfig();
214
+ }
215
+
216
+ return configs;
217
+ }
218
+
219
+ /** Check if a channel is enabled via env var */
220
+ function isChannelEnabled(prefix: string, defaultEnabled = false): boolean {
221
+ const value = process.env[`${prefix}_ENABLED`];
222
+ if (value === undefined) return defaultEnabled;
223
+ return value === "true" || value === "1";
224
+ }
225
+
226
+ /** Get env var with fallback */
227
+ function getEnv(key: string, fallback?: string): string | undefined {
228
+ return process.env[key] ?? fallback;
229
+ }
230
+
231
+ /** Get required env var (throws if missing) */
232
+ function requireEnv(key: string): string {
233
+ const value = process.env[key];
234
+ if (!value) {
235
+ throw new Error(`Required environment variable ${key} is not set`);
236
+ }
237
+ return value;
238
+ }
239
+
240
+ /** Parse comma-separated list from env */
241
+ function parseList(value?: string): string[] | undefined {
242
+ if (!value) return undefined;
243
+ return value.split(",").map((s) => s.trim()).filter(Boolean);
244
+ }
245
+
246
+ /** Parse number list from env */
247
+ function parseNumberList(value?: string): number[] | undefined {
248
+ const list = parseList(value);
249
+ return list?.map((s) => parseInt(s, 10)).filter((n) => !isNaN(n));
250
+ }
251
+
252
+ // Individual channel loaders
253
+
254
+ function loadTelegramConfig(): TelegramChannelConfig {
255
+ return {
256
+ platform: "telegram",
257
+ accountId: getEnv("TELEGRAM_ACCOUNT_ID", "default")!,
258
+ enabled: true,
259
+ botToken: requireEnv("TELEGRAM_BOT_TOKEN"),
260
+ pollingInterval: parseInt(getEnv("TELEGRAM_POLLING_INTERVAL", "1000")!, 10),
261
+ allowedUsers: parseNumberList(getEnv("TELEGRAM_ALLOWED_USERS")),
262
+ allowedChats: parseNumberList(getEnv("TELEGRAM_ALLOWED_CHATS")),
263
+ };
264
+ }
265
+
266
+ function loadDiscordConfig(): DiscordChannelConfig {
267
+ return {
268
+ platform: "discord",
269
+ accountId: getEnv("DISCORD_ACCOUNT_ID", "default")!,
270
+ enabled: true,
271
+ botToken: requireEnv("DISCORD_BOT_TOKEN"),
272
+ applicationId: getEnv("DISCORD_APPLICATION_ID"),
273
+ guildId: getEnv("DISCORD_GUILD_ID"),
274
+ channelIds: parseList(getEnv("DISCORD_CHANNEL_IDS")),
275
+ enableSlashCommands: getEnv("DISCORD_ENABLE_SLASH_COMMANDS", "true") === "true",
276
+ };
277
+ }
278
+
279
+ function loadWhatsAppConfig(): WhatsAppChannelConfig {
280
+ return {
281
+ platform: "whatsapp",
282
+ accountId: getEnv("WHATSAPP_ACCOUNT_ID", "default")!,
283
+ enabled: true,
284
+ webhookUrl: getEnv("WHATSAPP_WEBHOOK_URL"),
285
+ phoneNumberId: getEnv("WHATSAPP_PHONE_NUMBER_ID"),
286
+ accessToken: getEnv("WHATSAPP_ACCESS_TOKEN"),
287
+ sessionPath: getEnv("WHATSAPP_SESSION_PATH"),
288
+ };
289
+ }
290
+
291
+ function loadIMessageConfig(): IMessageChannelConfig {
292
+ return {
293
+ platform: "imessage",
294
+ accountId: getEnv("IMESSAGE_ACCOUNT_ID", "default")!,
295
+ enabled: true,
296
+ serverUrl: getEnv("IMESSAGE_SERVER_URL"),
297
+ password: getEnv("IMESSAGE_PASSWORD"),
298
+ local: getEnv("IMESSAGE_LOCAL", "false") === "true",
299
+ };
300
+ }
301
+
302
+ function loadSlackConfig(): SlackChannelConfig {
303
+ return {
304
+ platform: "slack",
305
+ accountId: getEnv("SLACK_ACCOUNT_ID", "default")!,
306
+ enabled: true,
307
+ botToken: requireEnv("SLACK_BOT_TOKEN"),
308
+ appToken: getEnv("SLACK_APP_TOKEN"),
309
+ signingSecret: getEnv("SLACK_SIGNING_SECRET"),
310
+ channelIds: parseList(getEnv("SLACK_CHANNEL_IDS")),
311
+ };
312
+ }
313
+
314
+ function loadSignalConfig(): SignalChannelConfig {
315
+ return {
316
+ platform: "signal",
317
+ accountId: getEnv("SIGNAL_ACCOUNT_ID", "default")!,
318
+ enabled: true,
319
+ phoneNumber: requireEnv("SIGNAL_PHONE_NUMBER"),
320
+ cliPath: getEnv("SIGNAL_CLI_PATH"),
321
+ apiUrl: getEnv("SIGNAL_API_URL"),
322
+ };
323
+ }
324
+
325
+ function loadCLIConfig(): CLIChannelConfig {
326
+ return {
327
+ platform: "cli",
328
+ accountId: getEnv("CLI_ACCOUNT_ID", "default")!,
329
+ enabled: true,
330
+ interactive: getEnv("CLI_INTERACTIVE", "true") === "true",
331
+ prompt: getEnv("CLI_PROMPT", "> "),
332
+ historyPath: getEnv("CLI_HISTORY_PATH"),
333
+ };
334
+ }
335
+
336
+ function loadWebConfig(): WebChannelConfig {
337
+ return {
338
+ platform: "web",
339
+ accountId: getEnv("WEB_ACCOUNT_ID", "default")!,
340
+ enabled: true,
341
+ port: parseInt(getEnv("WEB_PORT", "3000")!, 10),
342
+ host: getEnv("WEB_HOST", "0.0.0.0"),
343
+ apiKey: getEnv("WEB_API_KEY"),
344
+ cors: getEnv("WEB_CORS", "true") === "true",
345
+ };
346
+ }
347
+
348
+ // ============================================================
349
+ // CONFIG UTILITIES
350
+ // ============================================================
351
+
352
+ /** Get ChannelId from config */
353
+ export function getChannelId(config: ChannelConfig): ChannelId {
354
+ return createChannelId(config.platform, config.accountId, config.instanceId);
355
+ }
356
+
357
+ /** Validate channel config */
358
+ export function validateChannelConfig(config: ChannelConfig): string[] {
359
+ const errors: string[] = [];
360
+
361
+ switch (config.platform) {
362
+ case "telegram":
363
+ if (!config.botToken) errors.push("Telegram: botToken is required");
364
+ break;
365
+ case "discord":
366
+ if (!config.botToken) errors.push("Discord: botToken is required");
367
+ break;
368
+ case "whatsapp":
369
+ if (!config.webhookUrl && !config.sessionPath) {
370
+ errors.push("WhatsApp: webhookUrl or sessionPath is required");
371
+ }
372
+ break;
373
+ case "signal":
374
+ if (!config.phoneNumber) errors.push("Signal: phoneNumber is required");
375
+ break;
376
+ case "slack":
377
+ if (!config.botToken) errors.push("Slack: botToken is required");
378
+ break;
379
+ }
380
+
381
+ return errors;
382
+ }
383
+
384
+ /** Filter configs by enabled status */
385
+ export function getEnabledChannels(configs: ChannelConfigMap): ChannelConfig[] {
386
+ return Object.values(configs).filter((c) => c.enabled);
387
+ }
388
+
389
+ /** Filter configs by platform */
390
+ export function getChannelsByPlatform(
391
+ configs: ChannelConfigMap,
392
+ platform: ChannelPlatform
393
+ ): ChannelConfig[] {
394
+ return Object.values(configs).filter((c) => c.platform === platform);
395
+ }
396
+
397
+ /** Create config programmatically */
398
+ export function createChannelConfig<T extends ChannelConfig>(config: T): T {
399
+ const errors = validateChannelConfig(config);
400
+ if (errors.length > 0) {
401
+ throw new Error(`Invalid channel config: ${errors.join(", ")}`);
402
+ }
403
+ return config;
404
+ }
405
+
406
+ /** Parse config from JSON string */
407
+ export function parseChannelConfig(json: string): ChannelConfig {
408
+ const config = JSON.parse(json) as ChannelConfig;
409
+ const errors = validateChannelConfig(config);
410
+ if (errors.length > 0) {
411
+ throw new Error(`Invalid channel config: ${errors.join(", ")}`);
412
+ }
413
+ return config;
414
+ }
415
+
416
+ /** Parse multiple configs from JSON string */
417
+ export function parseChannelConfigs(json: string): ChannelConfigMap {
418
+ return JSON.parse(json) as ChannelConfigMap;
419
+ }
420
+
421
+ /** Serialize config to JSON */
422
+ export function serializeChannelConfig(config: ChannelConfig): string {
423
+ return JSON.stringify(config, null, 2);
424
+ }
425
+
426
+ /** Serialize multiple configs to JSON */
427
+ export function serializeChannelConfigs(configs: ChannelConfigMap): string {
428
+ return JSON.stringify(configs, null, 2);
429
+ }
package/src/example.ts ADDED
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Example: Composable Channel-LLM Architecture
3
+ *
4
+ * This demonstrates how to:
5
+ * 1. Create a channel connector (Discord)
6
+ * 2. Create an LLM handler (GLM)
7
+ * 3. Connect them via a bridge
8
+ */
9
+
10
+ import type {
11
+ ChannelBridge,
12
+ ChannelCapabilities,
13
+ ChannelConnector,
14
+ ChannelId,
15
+ ChannelMessage,
16
+ ChannelResponse,
17
+ LLMHandler,
18
+ MessageHandler,
19
+ StreamChunk,
20
+ } from "./index.js";
21
+ import {
22
+ createChannelId,
23
+ createMessageRef,
24
+ DEFAULT_CAPABILITIES,
25
+ RICH_CAPABILITIES,
26
+ } from "./index.js";
27
+
28
+ // ============================================================
29
+ // EXAMPLE: Discord Channel Connector
30
+ // ============================================================
31
+
32
+ class DiscordChannel implements ChannelConnector {
33
+ readonly id: ChannelId;
34
+ readonly label = "Discord";
35
+ readonly capabilities: ChannelCapabilities = RICH_CAPABILITIES;
36
+
37
+ private messageHandler?: MessageHandler;
38
+ private connected = false;
39
+
40
+ constructor(accountId: string) {
41
+ this.id = createChannelId("discord", accountId);
42
+ }
43
+
44
+ async start(): Promise<void> {
45
+ // In real impl: connect to Discord gateway
46
+ this.connected = true;
47
+ console.log(`[${this.label}] Started`);
48
+ }
49
+
50
+ async stop(): Promise<void> {
51
+ this.connected = false;
52
+ console.log(`[${this.label}] Stopped`);
53
+ }
54
+
55
+ onMessage(handler: MessageHandler): void {
56
+ this.messageHandler = handler;
57
+ }
58
+
59
+ async send(response: ChannelResponse): Promise<void> {
60
+ // In real impl: send to Discord API
61
+ console.log(`[${this.label}] Sending: ${response.content.text}`);
62
+ }
63
+
64
+ async stream(
65
+ response: ChannelResponse,
66
+ chunks: AsyncIterable<StreamChunk>
67
+ ): Promise<void> {
68
+ // In real impl: edit message with streaming content
69
+ let fullText = "";
70
+ for await (const chunk of chunks) {
71
+ fullText += chunk.text;
72
+ console.log(`[${this.label}] Stream: ${fullText}`);
73
+ }
74
+ }
75
+
76
+ isConnected(): boolean {
77
+ return this.connected;
78
+ }
79
+
80
+ // Simulate receiving a message (for testing)
81
+ async simulateMessage(text: string, senderId: string): Promise<void> {
82
+ if (!this.messageHandler) return;
83
+
84
+ const message: ChannelMessage = {
85
+ messageId: `msg-${Date.now()}`,
86
+ channelId: this.id,
87
+ timestamp: new Date(),
88
+ sender: { id: senderId, username: `user_${senderId}` },
89
+ text,
90
+ context: { isDM: true },
91
+ };
92
+
93
+ const response = await this.messageHandler(message);
94
+ if (response) {
95
+ await this.send(response);
96
+ }
97
+ }
98
+ }
99
+
100
+ // ============================================================
101
+ // EXAMPLE: GLM LLM Handler
102
+ // ============================================================
103
+
104
+ class GLMHandler implements LLMHandler {
105
+ readonly id = "glm-4.7";
106
+ readonly model = "glm-4-flash";
107
+
108
+ private ready = true;
109
+
110
+ async process(message: ChannelMessage): Promise<ChannelResponse> {
111
+ // In real impl: call GLM API
112
+ const startTime = Date.now();
113
+
114
+ // Simulate LLM response
115
+ const responseText = this.generateResponse(message.text);
116
+
117
+ return {
118
+ content: {
119
+ text: responseText,
120
+ replyToOriginal: true,
121
+ },
122
+ replyTo: createMessageRef(message.messageId, message.channelId),
123
+ isComplete: true,
124
+ metadata: {
125
+ model: this.model,
126
+ latency: Date.now() - startTime,
127
+ },
128
+ };
129
+ }
130
+
131
+ async* stream(message: ChannelMessage): AsyncIterable<StreamChunk> {
132
+ // In real impl: stream from GLM API
133
+ const response = await this.process(message);
134
+ const words = response.content.text.split(" ");
135
+
136
+ for (let i = 0; i < words.length; i++) {
137
+ yield {
138
+ text: words[i] + " ",
139
+ done: i === words.length - 1,
140
+ seq: i,
141
+ };
142
+ await new Promise((r) => setTimeout(r, 50));
143
+ }
144
+ }
145
+
146
+ isReady(): boolean {
147
+ return this.ready;
148
+ }
149
+
150
+ private generateResponse(input: string): string {
151
+ // Simple echo + prefix for demo
152
+ if (input.toLowerCase().includes("hello")) {
153
+ return "Hello! How can I help you today?";
154
+ }
155
+ if (input.toLowerCase().includes("status")) {
156
+ return "All systems operational. GLM 4.7 is ready to assist.";
157
+ }
158
+ return `I received your message: "${input}"`;
159
+ }
160
+ }
161
+
162
+ // ============================================================
163
+ // EXAMPLE: Simple Bridge Implementation
164
+ // ============================================================
165
+
166
+ class SimpleBridge implements ChannelBridge {
167
+ private channels: Map<string, ChannelConnector> = new Map();
168
+ private handlers: Map<string, LLMHandler> = new Map();
169
+ private defaultHandlers: Map<string, string> = new Map();
170
+
171
+ registerChannel(channel: ChannelConnector): void {
172
+ const key = this.channelKey(channel.id);
173
+ this.channels.set(key, channel);
174
+
175
+ // Set up message routing
176
+ channel.onMessage(async (message) => {
177
+ const handlerId = this.defaultHandlers.get(key);
178
+ if (!handlerId) {
179
+ console.warn(`No handler for channel ${key}`);
180
+ return;
181
+ }
182
+
183
+ const handler = this.handlers.get(handlerId);
184
+ if (!handler) {
185
+ console.warn(`Handler ${handlerId} not found`);
186
+ return;
187
+ }
188
+
189
+ return handler.process(message);
190
+ });
191
+
192
+ console.log(`[Bridge] Registered channel: ${channel.label}`);
193
+ }
194
+
195
+ registerHandler(handler: LLMHandler): void {
196
+ this.handlers.set(handler.id, handler);
197
+ console.log(`[Bridge] Registered handler: ${handler.id}`);
198
+ }
199
+
200
+ setDefaultHandler(channelId: ChannelId, handlerId: string): void {
201
+ this.defaultHandlers.set(this.channelKey(channelId), handlerId);
202
+ console.log(`[Bridge] Set default handler: ${this.channelKey(channelId)} → ${handlerId}`);
203
+ }
204
+
205
+ async start(): Promise<void> {
206
+ for (const channel of this.channels.values()) {
207
+ await channel.start();
208
+ }
209
+ }
210
+
211
+ async stop(): Promise<void> {
212
+ for (const channel of this.channels.values()) {
213
+ await channel.stop();
214
+ }
215
+ }
216
+
217
+ getChannels(): ChannelConnector[] {
218
+ return Array.from(this.channels.values());
219
+ }
220
+
221
+ getHandlers(): LLMHandler[] {
222
+ return Array.from(this.handlers.values());
223
+ }
224
+
225
+ private channelKey(id: ChannelId): string {
226
+ return `${id.platform}:${id.accountId}`;
227
+ }
228
+ }
229
+
230
+ // ============================================================
231
+ // USAGE EXAMPLE
232
+ // ============================================================
233
+
234
+ async function main() {
235
+ console.log("=== Composable Channel-LLM Example ===\n");
236
+
237
+ // 1. Create bridge
238
+ const bridge = new SimpleBridge();
239
+
240
+ // 2. Create and register LLM handler
241
+ const glmHandler = new GLMHandler();
242
+ bridge.registerHandler(glmHandler);
243
+
244
+ // 3. Create and register channel
245
+ const discordChannel = new DiscordChannel("my-bot-123");
246
+ bridge.registerChannel(discordChannel);
247
+
248
+ // 4. Connect channel to handler
249
+ bridge.setDefaultHandler(discordChannel.id, glmHandler.id);
250
+
251
+ // 5. Start the bridge
252
+ await bridge.start();
253
+
254
+ // 6. Simulate messages
255
+ console.log("\n--- Simulating messages ---\n");
256
+ await discordChannel.simulateMessage("Hello!", "user-1");
257
+ await discordChannel.simulateMessage("What's the status?", "user-2");
258
+ await discordChannel.simulateMessage("Can you help me with a task?", "user-3");
259
+
260
+ // 7. Stop
261
+ console.log("\n--- Stopping ---\n");
262
+ await bridge.stop();
263
+ }
264
+
265
+ // Run example
266
+ main().catch(console.error);
267
+
268
+ /**
269
+ * Output:
270
+ *
271
+ * === Composable Channel-LLM Example ===
272
+ *
273
+ * [Bridge] Registered handler: glm-4.7
274
+ * [Bridge] Registered channel: Discord
275
+ * [Bridge] Set default handler: discord:my-bot-123 → glm-4.7
276
+ * [Discord] Started
277
+ *
278
+ * --- Simulating messages ---
279
+ *
280
+ * [Discord] Sending: Hello! How can I help you today?
281
+ * [Discord] Sending: All systems operational. GLM 4.7 is ready to assist.
282
+ * [Discord] Sending: I received your message: "Can you help me with a task?"
283
+ *
284
+ * --- Stopping ---
285
+ *
286
+ * [Discord] Stopped
287
+ */