Refactor index (#156)

* feat: add Telegram channel with agent swarm support

Add Telegram as a messaging channel that can run alongside WhatsApp
or standalone (TELEGRAM_ONLY mode). Includes bot pool support for
agent swarms where each subagent appears as a different bot identity
in the group.

- Add grammy dependency for Telegram Bot API
- Route messages through tg: JID prefix convention
- Add storeMessageDirect for non-Baileys channels
- Add sender field to IPC send_message for swarm identity
- Support TELEGRAM_BOT_TOKEN, TELEGRAM_ONLY, TELEGRAM_BOT_POOL config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add index.ts refactor plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract channel abstraction, IPC, and router from index.ts

Break the 1088-line monolith into focused modules:
- src/channels/whatsapp.ts: WhatsAppChannel class implementing Channel interface
- src/ipc.ts: IPC watcher and task processing with dependency injection
- src/router.ts: message formatting, outbound routing, channel lookup
- src/types.ts: Channel interface, OnInboundMessage, OnChatMetadata types

Also adds regression test suite (98 tests), updates all documentation
and skill files to reflect the new architecture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add test workflow for PRs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove accidentally committed pool-bot assets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ci): remove grammy from base dependencies

Grammy is installed by the /add-telegram skill, not a base dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-11 00:36:37 +02:00
committed by GitHub
parent 196abf67cf
commit 2b56fecfdc
28 changed files with 4273 additions and 1066 deletions

46
src/router.ts Normal file
View File

@@ -0,0 +1,46 @@
import { ASSISTANT_NAME } from './config.js';
import { Channel, NewMessage } from './types.js';
export function escapeXml(s: string): string {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
export function formatMessages(messages: NewMessage[]): string {
const lines = messages.map((m) =>
`<message sender="${escapeXml(m.sender_name)}" time="${m.timestamp}">${escapeXml(m.content)}</message>`,
);
return `<messages>\n${lines.join('\n')}\n</messages>`;
}
export function stripInternalTags(text: string): string {
return text.replace(/<internal>[\s\S]*?<\/internal>/g, '').trim();
}
export function formatOutbound(channel: Channel, rawText: string): string {
const text = stripInternalTags(rawText);
if (!text) return '';
const prefix =
channel.prefixAssistantName !== false ? `${ASSISTANT_NAME}: ` : '';
return `${prefix}${text}`;
}
export function routeOutbound(
channels: Channel[],
jid: string,
text: string,
): Promise<void> {
const channel = channels.find((c) => c.ownsJid(jid) && c.isConnected());
if (!channel) throw new Error(`No channel for JID: ${jid}`);
return channel.sendMessage(jid, text);
}
export function findChannel(
channels: Channel[],
jid: string,
): Channel | undefined {
return channels.find((c) => c.ownsJid(jid));
}