@johpaz/hive 1.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.
Files changed (156) hide show
  1. package/CONTRIBUTING.md +44 -0
  2. package/README.md +310 -0
  3. package/package.json +96 -0
  4. package/packages/cli/package.json +28 -0
  5. package/packages/cli/src/commands/agent-run.ts +168 -0
  6. package/packages/cli/src/commands/agents.ts +398 -0
  7. package/packages/cli/src/commands/chat.ts +142 -0
  8. package/packages/cli/src/commands/config.ts +50 -0
  9. package/packages/cli/src/commands/cron.ts +161 -0
  10. package/packages/cli/src/commands/dev.ts +95 -0
  11. package/packages/cli/src/commands/doctor.ts +133 -0
  12. package/packages/cli/src/commands/gateway.ts +443 -0
  13. package/packages/cli/src/commands/logs.ts +57 -0
  14. package/packages/cli/src/commands/mcp.ts +175 -0
  15. package/packages/cli/src/commands/message.ts +77 -0
  16. package/packages/cli/src/commands/onboard.ts +1868 -0
  17. package/packages/cli/src/commands/security.ts +144 -0
  18. package/packages/cli/src/commands/service.ts +50 -0
  19. package/packages/cli/src/commands/sessions.ts +116 -0
  20. package/packages/cli/src/commands/skills.ts +187 -0
  21. package/packages/cli/src/commands/update.ts +25 -0
  22. package/packages/cli/src/index.ts +185 -0
  23. package/packages/cli/src/utils/token.ts +6 -0
  24. package/packages/code-bridge/README.md +78 -0
  25. package/packages/code-bridge/package.json +18 -0
  26. package/packages/code-bridge/src/index.ts +95 -0
  27. package/packages/code-bridge/src/process-manager.ts +212 -0
  28. package/packages/code-bridge/src/schemas.ts +133 -0
  29. package/packages/core/package.json +46 -0
  30. package/packages/core/src/agent/agent-loop.ts +369 -0
  31. package/packages/core/src/agent/compaction.ts +140 -0
  32. package/packages/core/src/agent/context-compiler.ts +378 -0
  33. package/packages/core/src/agent/context-guard.ts +91 -0
  34. package/packages/core/src/agent/context.ts +138 -0
  35. package/packages/core/src/agent/conversation-store.ts +198 -0
  36. package/packages/core/src/agent/curator.ts +158 -0
  37. package/packages/core/src/agent/hooks.ts +166 -0
  38. package/packages/core/src/agent/index.ts +116 -0
  39. package/packages/core/src/agent/llm-client.ts +503 -0
  40. package/packages/core/src/agent/native-tools.ts +505 -0
  41. package/packages/core/src/agent/prompt-builder.ts +532 -0
  42. package/packages/core/src/agent/providers/index.ts +167 -0
  43. package/packages/core/src/agent/providers.ts +1 -0
  44. package/packages/core/src/agent/reflector.ts +170 -0
  45. package/packages/core/src/agent/service.ts +64 -0
  46. package/packages/core/src/agent/stuck-loop.ts +133 -0
  47. package/packages/core/src/agent/supervisor.ts +39 -0
  48. package/packages/core/src/agent/tracer.ts +102 -0
  49. package/packages/core/src/agent/workspace.ts +110 -0
  50. package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
  51. package/packages/core/src/canvas/canvas-manager.ts +319 -0
  52. package/packages/core/src/canvas/canvas-tools.ts +420 -0
  53. package/packages/core/src/canvas/emitter.ts +115 -0
  54. package/packages/core/src/canvas/index.ts +2 -0
  55. package/packages/core/src/channels/base.ts +138 -0
  56. package/packages/core/src/channels/discord.ts +260 -0
  57. package/packages/core/src/channels/index.ts +7 -0
  58. package/packages/core/src/channels/manager.ts +383 -0
  59. package/packages/core/src/channels/slack.ts +287 -0
  60. package/packages/core/src/channels/telegram.ts +502 -0
  61. package/packages/core/src/channels/webchat.ts +128 -0
  62. package/packages/core/src/channels/whatsapp.ts +375 -0
  63. package/packages/core/src/config/index.ts +12 -0
  64. package/packages/core/src/config/loader.ts +529 -0
  65. package/packages/core/src/events/event-bus.ts +169 -0
  66. package/packages/core/src/gateway/index.ts +5 -0
  67. package/packages/core/src/gateway/initializer.ts +290 -0
  68. package/packages/core/src/gateway/lane-queue.ts +169 -0
  69. package/packages/core/src/gateway/resolver.ts +108 -0
  70. package/packages/core/src/gateway/router.ts +124 -0
  71. package/packages/core/src/gateway/server.ts +3317 -0
  72. package/packages/core/src/gateway/session.ts +95 -0
  73. package/packages/core/src/gateway/slash-commands.ts +192 -0
  74. package/packages/core/src/heartbeat/index.ts +157 -0
  75. package/packages/core/src/index.ts +19 -0
  76. package/packages/core/src/integrations/catalog.ts +286 -0
  77. package/packages/core/src/integrations/env.ts +64 -0
  78. package/packages/core/src/integrations/index.ts +2 -0
  79. package/packages/core/src/memory/index.ts +1 -0
  80. package/packages/core/src/memory/notes.ts +68 -0
  81. package/packages/core/src/plugins/api.ts +128 -0
  82. package/packages/core/src/plugins/index.ts +2 -0
  83. package/packages/core/src/plugins/loader.ts +365 -0
  84. package/packages/core/src/resilience/circuit-breaker.ts +225 -0
  85. package/packages/core/src/security/google-chat.ts +269 -0
  86. package/packages/core/src/security/index.ts +192 -0
  87. package/packages/core/src/security/pairing.ts +250 -0
  88. package/packages/core/src/security/rate-limit.ts +270 -0
  89. package/packages/core/src/security/signal.ts +321 -0
  90. package/packages/core/src/state/store.ts +312 -0
  91. package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
  92. package/packages/core/src/storage/crypto.ts +101 -0
  93. package/packages/core/src/storage/db-context.ts +333 -0
  94. package/packages/core/src/storage/onboarding.ts +1087 -0
  95. package/packages/core/src/storage/schema.ts +541 -0
  96. package/packages/core/src/storage/seed.ts +571 -0
  97. package/packages/core/src/storage/sqlite.ts +387 -0
  98. package/packages/core/src/storage/usage.ts +212 -0
  99. package/packages/core/src/tools/bridge-events.ts +74 -0
  100. package/packages/core/src/tools/browser.ts +275 -0
  101. package/packages/core/src/tools/codebridge.ts +421 -0
  102. package/packages/core/src/tools/coordinator-tools.ts +179 -0
  103. package/packages/core/src/tools/cron.ts +611 -0
  104. package/packages/core/src/tools/exec.ts +140 -0
  105. package/packages/core/src/tools/fs.ts +364 -0
  106. package/packages/core/src/tools/index.ts +12 -0
  107. package/packages/core/src/tools/memory.ts +176 -0
  108. package/packages/core/src/tools/notify.ts +113 -0
  109. package/packages/core/src/tools/project-management.ts +376 -0
  110. package/packages/core/src/tools/project.ts +375 -0
  111. package/packages/core/src/tools/read.ts +158 -0
  112. package/packages/core/src/tools/web.ts +436 -0
  113. package/packages/core/src/tools/workspace.ts +171 -0
  114. package/packages/core/src/utils/benchmark.ts +80 -0
  115. package/packages/core/src/utils/crypto.ts +73 -0
  116. package/packages/core/src/utils/date.ts +42 -0
  117. package/packages/core/src/utils/index.ts +4 -0
  118. package/packages/core/src/utils/logger.ts +388 -0
  119. package/packages/core/src/utils/retry.ts +70 -0
  120. package/packages/core/src/voice/index.ts +583 -0
  121. package/packages/core/tsconfig.json +9 -0
  122. package/packages/mcp/package.json +26 -0
  123. package/packages/mcp/src/config.ts +13 -0
  124. package/packages/mcp/src/index.ts +1 -0
  125. package/packages/mcp/src/logger.ts +42 -0
  126. package/packages/mcp/src/manager.ts +434 -0
  127. package/packages/mcp/src/transports/index.ts +67 -0
  128. package/packages/mcp/src/transports/sse.ts +241 -0
  129. package/packages/mcp/src/transports/websocket.ts +159 -0
  130. package/packages/skills/package.json +21 -0
  131. package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
  132. package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
  133. package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
  134. package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
  135. package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
  136. package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
  137. package/packages/skills/src/bundled/memory/SKILL.md +42 -0
  138. package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
  139. package/packages/skills/src/bundled/shell/SKILL.md +43 -0
  140. package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
  141. package/packages/skills/src/bundled/voice/SKILL.md +25 -0
  142. package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
  143. package/packages/skills/src/index.ts +1 -0
  144. package/packages/skills/src/loader.ts +282 -0
  145. package/packages/tools/package.json +43 -0
  146. package/packages/tools/src/browser/browser.test.ts +111 -0
  147. package/packages/tools/src/browser/index.ts +272 -0
  148. package/packages/tools/src/canvas/index.ts +220 -0
  149. package/packages/tools/src/cron/cron.test.ts +164 -0
  150. package/packages/tools/src/cron/index.ts +304 -0
  151. package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
  152. package/packages/tools/src/filesystem/index.ts +379 -0
  153. package/packages/tools/src/git/index.ts +239 -0
  154. package/packages/tools/src/index.ts +4 -0
  155. package/packages/tools/src/shell/detect-env.ts +70 -0
  156. package/packages/tools/tsconfig.json +9 -0
@@ -0,0 +1,260 @@
1
+ import {
2
+ Client,
3
+ GatewayIntentBits,
4
+ Events,
5
+ type Message,
6
+ type TextChannel,
7
+ type DMChannel,
8
+ type NewsChannel,
9
+ } from "discord.js";
10
+ import { BaseChannel, type ChannelConfig, type IncomingMessage, type OutboundMessage } from "./base.ts";
11
+ import { logger } from "../utils/logger.ts";
12
+
13
+ export interface DiscordConfig extends ChannelConfig {
14
+ botToken: string;
15
+ applicationId?: string;
16
+ guilds?: Record<string, unknown>;
17
+ }
18
+
19
+ type DiscordTextChannel = TextChannel | DMChannel | NewsChannel;
20
+
21
+ export class DiscordChannel extends BaseChannel {
22
+ name = "discord";
23
+ accountId: string;
24
+ config: DiscordConfig;
25
+
26
+ private client?: Client;
27
+ private log = logger.child("discord");
28
+ private channelCache: Map<string, DiscordTextChannel> = new Map();
29
+
30
+ constructor(accountId: string, config: DiscordConfig) {
31
+ super();
32
+ this.accountId = accountId;
33
+ this.config = config;
34
+ }
35
+
36
+ async start(): Promise<void> {
37
+ if (!this.config.botToken) {
38
+ throw new Error("Discord bot token not configured");
39
+ }
40
+
41
+ this.client = new Client({
42
+ intents: [
43
+ GatewayIntentBits.Guilds,
44
+ GatewayIntentBits.GuildMessages,
45
+ GatewayIntentBits.DirectMessages,
46
+ GatewayIntentBits.MessageContent,
47
+ ],
48
+ });
49
+
50
+ this.client.on(Events.MessageCreate, async (message) => {
51
+ await this.handleDiscordMessage(message);
52
+ });
53
+
54
+ this.client.on(Events.Error, (error) => {
55
+ this.log.error(`Discord client error: ${error.message}`);
56
+ });
57
+
58
+ this.client.once(Events.ClientReady, () => {
59
+ this.log.info(`Discord bot started: ${this.client?.user?.tag ?? "unknown"}`);
60
+ this.running = true;
61
+ });
62
+
63
+ try {
64
+ await this.client.login(this.config.botToken);
65
+ } catch (error) {
66
+ this.log.error(`Failed to login to Discord: ${(error as Error).message}`);
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ private async handleDiscordMessage(message: Message): Promise<void> {
72
+ if (message.author.bot) return;
73
+
74
+ const isGuild = message.guild !== null;
75
+ const kind = isGuild ? "group" : "direct";
76
+ const peerId = isGuild
77
+ ? `${message.guildId}:${message.channelId}:${message.author.id}`
78
+ : message.author.id;
79
+
80
+ if (!isGuild && !this.isUserAllowed(message.author.id)) {
81
+ this.log.debug(`Message from unauthorized user: ${message.author.id}`);
82
+ return;
83
+ }
84
+
85
+ const sessionId = this.formatSessionId(peerId, kind);
86
+ if (message.channel.isTextBased()) {
87
+ this.channelCache.set(sessionId, message.channel as DiscordTextChannel);
88
+ }
89
+
90
+ const audioAttachment = message.attachments.find(a =>
91
+ a.contentType?.startsWith("audio/") ||
92
+ a.url.endsWith(".mp3") ||
93
+ a.url.endsWith(".ogg") ||
94
+ a.url.endsWith(".webm") ||
95
+ a.url.endsWith(".wav")
96
+ );
97
+
98
+ const incomingMessage: IncomingMessage = {
99
+ sessionId,
100
+ channel: "discord",
101
+ accountId: this.accountId,
102
+ peerId,
103
+ peerKind: kind,
104
+ content: message.content || "",
105
+ audio: audioAttachment ? { url: audioAttachment.url, mimeType: audioAttachment.contentType || "audio/webm" } : undefined,
106
+ metadata: {
107
+ discord: {
108
+ guildId: message.guildId,
109
+ channelId: message.channelId,
110
+ userId: message.author.id,
111
+ username: message.author.username,
112
+ messageId: message.id,
113
+ roles: message.member?.roles.cache.map(r => r.id),
114
+ },
115
+ },
116
+ replyToId: message.reference?.messageId
117
+ ? `discord:${message.reference.messageId}`
118
+ : undefined,
119
+ };
120
+
121
+ await this.handleMessage(incomingMessage);
122
+ }
123
+
124
+ async stop(): Promise<void> {
125
+ if (this.client) {
126
+ this.client.destroy();
127
+ this.running = false;
128
+ this.log.info("Discord bot stopped");
129
+ }
130
+ }
131
+
132
+ private async getChannel(sessionId: string): Promise<DiscordTextChannel | null> {
133
+ const cached = this.channelCache.get(sessionId);
134
+ if (cached) return cached;
135
+
136
+ if (!this.client) return null;
137
+
138
+ const parts = sessionId.split(":");
139
+ const peerPart = parts.slice(3).join(":");
140
+
141
+ let channelId: string;
142
+ if (peerPart.includes(":")) {
143
+ const channelPart = peerPart.split(":");
144
+ channelId = channelPart[1] ?? peerPart;
145
+ } else {
146
+ channelId = peerPart;
147
+ }
148
+
149
+ try {
150
+ const channel = await this.client.channels.fetch(channelId);
151
+ if (channel && channel.isTextBased()) {
152
+ this.channelCache.set(sessionId, channel as DiscordTextChannel);
153
+ return channel as DiscordTextChannel;
154
+ }
155
+ } catch {
156
+ // Channel not found
157
+ }
158
+
159
+ return null;
160
+ }
161
+
162
+ async startTyping(sessionId: string): Promise<void> {
163
+ const channel = await this.getChannel(sessionId);
164
+ if (!channel) return;
165
+
166
+ await channel.sendTyping();
167
+
168
+ const interval = setInterval(async () => {
169
+ try {
170
+ await channel.sendTyping();
171
+ } catch {
172
+ this.stopTyping(sessionId);
173
+ }
174
+ }, 8000);
175
+
176
+ this.typingIntervals.set(sessionId, interval);
177
+ }
178
+
179
+ async stopTyping(sessionId: string): Promise<void> {
180
+ const interval = this.typingIntervals.get(sessionId);
181
+ if (interval) {
182
+ clearInterval(interval);
183
+ this.typingIntervals.delete(sessionId);
184
+ }
185
+ }
186
+
187
+ async send(sessionId: string, message: OutboundMessage): Promise<void> {
188
+ await this.stopTyping(sessionId);
189
+
190
+ const channel = await this.getChannel(sessionId);
191
+
192
+ if (!channel) {
193
+ throw new Error(`Channel not found for session: ${sessionId}`);
194
+ }
195
+
196
+ const content = message.content ?? "";
197
+ const maxLength = 2000;
198
+
199
+ try {
200
+ if (content.length <= maxLength) {
201
+ await channel.send(content);
202
+ } else {
203
+ const chunks = this.chunkMessage(content, maxLength);
204
+ for (const chunk of chunks) {
205
+ await channel.send(chunk);
206
+ }
207
+ }
208
+ } catch (error) {
209
+ this.log.error(`Failed to send Discord message: ${(error as Error).message}`);
210
+ throw error;
211
+ }
212
+ }
213
+
214
+ private chunkMessage(content: string, maxLength: number): string[] {
215
+ const chunks: string[] = [];
216
+ let remaining = content;
217
+
218
+ while (remaining.length > 0) {
219
+ if (remaining.length <= maxLength) {
220
+ chunks.push(remaining);
221
+ break;
222
+ }
223
+
224
+ let splitPoint = remaining.lastIndexOf("\n", maxLength);
225
+ if (splitPoint === -1 || splitPoint < maxLength * 0.5) {
226
+ splitPoint = remaining.lastIndexOf(" ", maxLength);
227
+ }
228
+ if (splitPoint === -1 || splitPoint < maxLength * 0.5) {
229
+ splitPoint = maxLength;
230
+ }
231
+
232
+ chunks.push(remaining.slice(0, splitPoint));
233
+ remaining = remaining.slice(splitPoint).trim();
234
+ }
235
+
236
+ return chunks;
237
+ }
238
+
239
+ async sendAudio(sessionId: string, audio: Buffer, mimeType: string): Promise<void> {
240
+ const channel = await this.getChannel(sessionId);
241
+
242
+ if (!channel) {
243
+ throw new Error(`Channel not found for session: ${sessionId}`);
244
+ }
245
+
246
+ try {
247
+ const attachmentName = `response.${mimeType === "audio/mpeg" ? "mp3" : "ogg"}`;
248
+ await channel.send({
249
+ files: [{ attachment: audio, name: attachmentName }]
250
+ });
251
+ } catch (error) {
252
+ this.log.error(`Failed to send Discord audio: ${(error as Error).message}`);
253
+ throw error;
254
+ }
255
+ }
256
+ }
257
+
258
+ export function createDiscordChannel(accountId: string, config: DiscordConfig): DiscordChannel {
259
+ return new DiscordChannel(accountId, config);
260
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./base.ts";
2
+ export * from "./telegram.ts";
3
+ export * from "./discord.ts";
4
+ export * from "./webchat.ts";
5
+ export * from "./whatsapp.ts";
6
+ export * from "./slack.ts";
7
+ export * from "./manager.ts";
@@ -0,0 +1,383 @@
1
+ import type { Config } from "../config/loader.ts";
2
+ import { logger } from "../utils/logger.ts";
3
+ import type { IChannel, IncomingMessage, MessageHandler } from "./base.ts";
4
+ import { createTelegramChannel, type TelegramConfig } from "./telegram.ts";
5
+ import { createDiscordChannel, type DiscordConfig } from "./discord.ts";
6
+ import { createWebChatChannel, type WebChatConfig } from "./webchat.ts";
7
+ import { createWhatsAppChannel, type WhatsAppConfig } from "./whatsapp.ts";
8
+ import { createSlackChannel, type SlackConfig } from "./slack.ts";
9
+ import { getDb } from "../storage/sqlite.ts";
10
+ import { decryptConfig } from "../storage/crypto.ts";
11
+
12
+ export class ChannelManager {
13
+ private config: Config;
14
+ private channels: Map<string, IChannel> = new Map();
15
+ private messageHandler?: MessageHandler;
16
+ private log = logger.child("channels");
17
+
18
+ constructor(config: Config) {
19
+ this.config = config;
20
+ }
21
+
22
+ onMessage(handler: MessageHandler): void {
23
+ this.messageHandler = handler;
24
+ }
25
+
26
+ async initialize(): Promise<void> {
27
+ // Primero, intentar cargar canales desde la BD
28
+ await this.initializeFromDB();
29
+
30
+ // Si no hay canales en la BD, usar config
31
+ if (this.channels.size === 0) {
32
+ await this.initializeFromConfig();
33
+ }
34
+
35
+ this.log.info(`Initialized ${this.channels.size} channel(s)`);
36
+ }
37
+
38
+ private async initializeFromDB(): Promise<void> {
39
+ try {
40
+ const db = getDb();
41
+ // Load all active channels - config may be empty for webchat
42
+ const rows = db.query(`
43
+ SELECT id, type, config_encrypted, config_iv, enabled, active
44
+ FROM channels
45
+ WHERE enabled = 1 AND active = 1
46
+ `).all() as Array<{
47
+ id: string;
48
+ type: string;
49
+ config_encrypted: string | null;
50
+ config_iv: string | null;
51
+ enabled: number;
52
+ active: number;
53
+ }>;
54
+
55
+ for (const row of rows) {
56
+ let config: Record<string, unknown> = {};
57
+
58
+ if (row.config_encrypted && row.config_iv) {
59
+ try {
60
+ config = await decryptConfig(row.config_encrypted, row.config_iv);
61
+ this.log.debug(`Decrypted config for ${row.type}:${row.id}:`, Object.keys(config));
62
+ } catch (error) {
63
+ this.log.warn(`Failed to decrypt config for channel ${row.id}:`, (error as Error).message);
64
+ }
65
+ }
66
+
67
+ // Use channel id as accountId
68
+ const accountId = row.id;
69
+ this.log.info(`Creating channel ${row.type}:${accountId} with config keys:`, Object.keys(config));
70
+ await this.createChannel(row.type, accountId, config);
71
+ }
72
+ } catch (error) {
73
+ this.log.debug("No channels found in DB or DB not initialized:", (error as Error).message);
74
+ }
75
+ }
76
+
77
+ private async initializeFromConfig(): Promise<void> {
78
+ const channelConfigs = this.config.channels ?? {};
79
+
80
+ for (const [channelName, channelConfig] of Object.entries(channelConfigs)) {
81
+ // If enabled is explicitly false, skip
82
+ if (channelConfig.enabled === false) {
83
+ this.log.debug(`Channel ${channelName} is disabled`);
84
+ continue;
85
+ }
86
+
87
+ const accounts = channelConfig.accounts;
88
+ if (!accounts || Object.keys(accounts).length === 0) {
89
+ this.log.warn(`Channel ${channelName} has no accounts configured`);
90
+ continue;
91
+ }
92
+
93
+ for (const [accountId, accountConfig] of Object.entries(accounts)) {
94
+ const fullConfig = { ...channelConfig, ...accountConfig };
95
+ await this.createChannel(channelName, accountId, fullConfig);
96
+ }
97
+ }
98
+ }
99
+
100
+ private async createChannel(
101
+ channelName: string,
102
+ accountId: string,
103
+ config: Record<string, unknown>
104
+ ): Promise<void> {
105
+ let channel: IChannel;
106
+
107
+ try {
108
+ switch (channelName) {
109
+ case "telegram":
110
+ channel = createTelegramChannel(accountId, {
111
+ enabled: true,
112
+ botToken: config.botToken as string,
113
+ dmPolicy: (config.dmPolicy as "open" | "pairing" | "allowlist") ?? "open",
114
+ allowFrom: (config.allowFrom as string[]) ?? [],
115
+ groups: (config.groups as boolean) ?? false,
116
+ } as TelegramConfig);
117
+ break;
118
+
119
+ case "discord":
120
+ channel = createDiscordChannel(accountId, {
121
+ enabled: true,
122
+ botToken: config.botToken as string,
123
+ applicationId: config.applicationId as string,
124
+ dmPolicy: (config.dmPolicy as "open" | "pairing" | "allowlist") ?? "allowlist",
125
+ allowFrom: (config.allowFrom as string[]) ?? [],
126
+ } as DiscordConfig);
127
+ break;
128
+
129
+ case "webchat":
130
+ channel = createWebChatChannel({
131
+ enabled: true,
132
+ dmPolicy: "open",
133
+ allowFrom: [],
134
+ } as WebChatConfig);
135
+ break;
136
+
137
+ case "whatsapp":
138
+ channel = createWhatsAppChannel({
139
+ enabled: true,
140
+ accountId,
141
+ agentId: (config.agentId as string) ?? "main",
142
+ dmPolicy: (config.dmPolicy as "open" | "pairing" | "allowlist") ?? "allowlist",
143
+ allowFrom: (config.allowFrom as string[]) ?? [],
144
+ reconnectMaxAttempts: (config.reconnectMaxAttempts as number) ?? 10,
145
+ reconnectBaseDelayMs: (config.reconnectBaseDelayMs as number) ?? 5000,
146
+ } as WhatsAppConfig);
147
+ break;
148
+
149
+ case "slack":
150
+ channel = createSlackChannel({
151
+ enabled: true,
152
+ botToken: config.botToken as string,
153
+ appToken: config.appToken as string,
154
+ signingSecret: config.signingSecret as string,
155
+ port: (config.port as number) ?? 3000,
156
+ dmPolicy: (config.dmPolicy as "open" | "pairing" | "allowlist") ?? "allowlist",
157
+ allowFrom: (config.allowFrom as string[]) ?? [],
158
+ } as SlackConfig);
159
+ break;
160
+
161
+ default:
162
+ this.log.warn(`Unknown channel type: ${channelName}`);
163
+ return;
164
+ }
165
+
166
+ channel.onMessage(async (message: IncomingMessage) => {
167
+ if (this.messageHandler) {
168
+ await this.messageHandler(message);
169
+ }
170
+ });
171
+
172
+ const key = `${channelName}:${accountId}`;
173
+ this.channels.set(key, channel);
174
+
175
+ this.log.info(`Created channel: ${key}`);
176
+ } catch (error) {
177
+ this.log.error(`Failed to create channel ${channelName}:${accountId}: ${(error as Error).message}`);
178
+ }
179
+ }
180
+
181
+ async startAll(): Promise<void> {
182
+ const promises: Promise<void>[] = [];
183
+
184
+ for (const [key, channel] of this.channels) {
185
+ if (channel.isRunning()) {
186
+ this.log.info(`Channel ${key} is already running, skipping`);
187
+ continue;
188
+ }
189
+ promises.push(
190
+ channel.start().catch((error) => {
191
+ this.log.error(`Failed to start channel ${key}: ${error.message}`);
192
+ })
193
+ );
194
+ }
195
+
196
+ await Promise.allSettled(promises);
197
+ this.log.info("All channels started");
198
+ }
199
+
200
+ async stopAll(): Promise<void> {
201
+ const promises: Promise<void>[] = [];
202
+
203
+ for (const [key, channel] of this.channels) {
204
+ promises.push(
205
+ channel.stop().catch((error) => {
206
+ this.log.error(`Failed to stop channel ${key}: ${error.message}`);
207
+ })
208
+ );
209
+ }
210
+
211
+ await Promise.allSettled(promises);
212
+ this.log.info("All channels stopped");
213
+ }
214
+
215
+ getChannel(channelName: string, accountId?: string): IChannel | undefined {
216
+ if (accountId) {
217
+ return this.channels.get(`${channelName}:${accountId}`);
218
+ }
219
+
220
+ for (const [key, channel] of this.channels) {
221
+ if (key.startsWith(channelName)) {
222
+ return channel;
223
+ }
224
+ }
225
+
226
+ return undefined;
227
+ }
228
+
229
+ async removeChannel(channelName: string, accountId: string): Promise<void> {
230
+ const key = `${channelName}:${accountId}`;
231
+ await this.stopChannel(channelName, accountId);
232
+ this.channels.delete(key);
233
+ this.log.info(`Removed channel: ${key}`);
234
+ }
235
+
236
+ getAccountConfig(channelName: string, accountId: string): any {
237
+ const channelConfigs = (this.config.channels ?? {}) as Record<string, any>;
238
+ const channelConfig = channelConfigs[channelName];
239
+ if (!channelConfig) return null;
240
+
241
+ const accounts = channelConfig.accounts;
242
+ if (!accounts) return null;
243
+ return accounts[accountId] || null;
244
+ }
245
+
246
+ async startChannel(channelName: string, accountId: string): Promise<void> {
247
+ const key = `${channelName}:${accountId}`;
248
+ let channel = this.channels.get(key);
249
+
250
+ if (!channel) {
251
+ const channelConfigs = (this.config.channels ?? {}) as Record<string, any>;
252
+ const channelConfig = channelConfigs[channelName];
253
+ if (!channelConfig) {
254
+ throw new Error(`Channel configuration not found: ${channelName}`);
255
+ }
256
+
257
+ const accounts = channelConfig.accounts;
258
+ if (!accounts) {
259
+ throw new Error(`Accounts configuration not found for channel ${channelName}`);
260
+ }
261
+ const accountConfig = accounts[accountId];
262
+ if (!accountConfig) {
263
+ throw new Error(`Account configuration not found: ${accountId} for channel ${channelName}`);
264
+ }
265
+
266
+ const fullConfig = { ...channelConfig, ...(accountConfig ?? {}) };
267
+ await this.createChannel(channelName, accountId, fullConfig as any);
268
+ channel = this.channels.get(key);
269
+ }
270
+
271
+ if (!channel) {
272
+ throw new Error(`Failed to instantiate channel: ${key}`);
273
+ }
274
+
275
+ if (channel.isRunning()) {
276
+ this.log.info(`Channel ${key} is already running`);
277
+ return;
278
+ }
279
+
280
+ await channel.start();
281
+ this.log.info(`Started channel: ${key}`);
282
+ }
283
+
284
+ async stopChannel(channelName: string, accountId: string): Promise<void> {
285
+ const key = `${channelName}:${accountId}`;
286
+ const channel = this.channels.get(key);
287
+
288
+ if (!channel) {
289
+ this.log.warn(`Channel ${key} not found or not instantiated`);
290
+ return;
291
+ }
292
+
293
+ if (!channel.isRunning()) {
294
+ this.log.info(`Channel ${key} is not running`);
295
+ return;
296
+ }
297
+
298
+ await channel.stop();
299
+ this.log.info(`Stopped channel: ${key}`);
300
+ }
301
+
302
+ listAllAvailableChannels(): Array<{ name: string; accountId: string; running: boolean; enabled: boolean }> {
303
+ const available: Array<{ name: string; accountId: string; running: boolean; enabled: boolean }> = [];
304
+ const channelConfigs = (this.config.channels ?? {}) as Record<string, any>;
305
+
306
+ for (const [channelName, channelConfig] of Object.entries(channelConfigs)) {
307
+ const accounts = channelConfig.accounts;
308
+ if (!accounts) continue;
309
+ for (const accountId of Object.keys(accounts)) {
310
+ const key = `${channelName}:${accountId}`;
311
+ const channel = this.channels.get(key);
312
+ available.push({
313
+ name: channelName,
314
+ accountId: accountId,
315
+ running: channel ? channel.isRunning() : false,
316
+ enabled: channelConfig.enabled !== false,
317
+ });
318
+ }
319
+ }
320
+ return available;
321
+ }
322
+
323
+ listChannels(): Array<{ name: string; accountId: string; running: boolean }> {
324
+ return Array.from(this.channels.entries()).map(([key, channel]) => {
325
+ const [name, accountId] = key.split(":");
326
+ return {
327
+ name: name ?? "unknown",
328
+ accountId: accountId ?? "unknown",
329
+ running: channel.isRunning(),
330
+ };
331
+ });
332
+ }
333
+
334
+ async send(
335
+ channelName: string,
336
+ sessionId: string,
337
+ message: unknown
338
+ ): Promise<void> {
339
+ const channel = this.getChannel(channelName);
340
+
341
+ if (!channel) {
342
+ throw new Error(`Channel not found: ${channelName}`);
343
+ }
344
+
345
+ await channel.send(sessionId, message as any);
346
+ }
347
+
348
+ async startTyping(channelName: string, sessionId: string): Promise<void> {
349
+ const channel = this.getChannel(channelName);
350
+ if (channel?.startTyping) {
351
+ await channel.startTyping(sessionId);
352
+ }
353
+ }
354
+
355
+ async stopTyping(channelName: string, sessionId: string): Promise<void> {
356
+ const channel = this.getChannel(channelName);
357
+ if (channel?.stopTyping) {
358
+ await channel.stopTyping(sessionId);
359
+ }
360
+ }
361
+
362
+ async markAsRead(channelName: string, sessionId: string, messageId?: string): Promise<void> {
363
+ const channel = this.getChannel(channelName);
364
+ if (channel?.markAsRead) {
365
+ await channel.markAsRead(sessionId, messageId);
366
+ }
367
+ }
368
+
369
+ async sendAudio(channelName: string, sessionId: string, audio: Buffer, mimeType: string): Promise<void> {
370
+ const channel = this.getChannel(channelName);
371
+ if (!channel) {
372
+ throw new Error(`Channel not found: ${channelName}`);
373
+ }
374
+ if (!channel.sendAudio) {
375
+ throw new Error(`Channel ${channelName} does not support audio`);
376
+ }
377
+ await channel.sendAudio(sessionId, audio, mimeType);
378
+ }
379
+ }
380
+
381
+ export function createChannelManager(config: Config): ChannelManager {
382
+ return new ChannelManager(config);
383
+ }