opencode-router 0.11.77 → 0.11.79

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/dist/slack.js ADDED
@@ -0,0 +1,169 @@
1
+ import { SocketModeClient } from "@slack/socket-mode";
2
+ import { WebClient } from "@slack/web-api";
3
+ // `peerId` encoding:
4
+ // - DMs: D12345678
5
+ // - Threads in channels: C12345678|1700000000.000100
6
+ // Using `|` avoids clashing with ALLOW_FROM's channel:peer parsing.
7
+ export function formatSlackPeerId(peer) {
8
+ if (!peer.threadTs)
9
+ return peer.channelId;
10
+ return `${peer.channelId}|${peer.threadTs}`;
11
+ }
12
+ export function parseSlackPeerId(peerId) {
13
+ const trimmed = peerId.trim();
14
+ if (!trimmed)
15
+ return { channelId: "" };
16
+ const [channelId, threadTs] = trimmed.split("|");
17
+ if (channelId && threadTs)
18
+ return { channelId, threadTs };
19
+ return { channelId: channelId || trimmed };
20
+ }
21
+ export function stripSlackMention(text, botUserId) {
22
+ let next = text ?? "";
23
+ if (botUserId) {
24
+ const token = `<@${botUserId}>`;
25
+ next = next.split(token).join(" ");
26
+ }
27
+ next = next.replace(/^\s*[:,-]+\s*/, "");
28
+ return next.trim();
29
+ }
30
+ const MAX_TEXT_LENGTH = 39_000;
31
+ export function createSlackAdapter(identity, config, logger, onMessage, deps = { WebClient, SocketModeClient }) {
32
+ const botToken = identity.botToken?.trim() ?? "";
33
+ const appToken = identity.appToken?.trim() ?? "";
34
+ if (!botToken) {
35
+ throw new Error("Slack bot token is required for Slack adapter");
36
+ }
37
+ if (!appToken) {
38
+ throw new Error("Slack app token is required for Slack adapter");
39
+ }
40
+ const log = logger.child({ channel: "slack", identityId: identity.id });
41
+ const web = new deps.WebClient(botToken);
42
+ const socket = new deps.SocketModeClient({ appToken });
43
+ let botUserId = null;
44
+ let started = false;
45
+ const safeAck = async (ack) => {
46
+ if (typeof ack !== "function")
47
+ return;
48
+ try {
49
+ await ack();
50
+ }
51
+ catch (error) {
52
+ log.warn({ error }, "slack ack failed");
53
+ }
54
+ };
55
+ const shouldIgnore = (event) => {
56
+ const channelId = typeof event.channel === "string" ? event.channel : "";
57
+ const textRaw = typeof event.text === "string" ? event.text : "";
58
+ const userId = typeof event.user === "string" ? event.user : null;
59
+ const botId = typeof event.bot_id === "string" ? event.bot_id : null;
60
+ const subtype = typeof event.subtype === "string" ? event.subtype : null;
61
+ // Avoid loops / non-user messages.
62
+ if (botId)
63
+ return { ok: true };
64
+ if (subtype && subtype !== "")
65
+ return { ok: true };
66
+ if (userId && botUserId && userId === botUserId)
67
+ return { ok: true };
68
+ if (!channelId || !textRaw.trim())
69
+ return { ok: true };
70
+ return { ok: false, channelId, textRaw, userId };
71
+ };
72
+ socket.on("message", async (args) => {
73
+ const ack = args?.ack;
74
+ await safeAck(ack);
75
+ const event = args?.event;
76
+ if (!event || typeof event !== "object")
77
+ return;
78
+ const filtered = shouldIgnore(event);
79
+ if (filtered.ok)
80
+ return;
81
+ // Only respond to direct messages by default.
82
+ const isDm = filtered.channelId.startsWith("D");
83
+ if (!isDm)
84
+ return;
85
+ const threadTs = typeof event.thread_ts === "string" ? event.thread_ts : null;
86
+ const peerId = formatSlackPeerId({ channelId: filtered.channelId, ...(threadTs ? { threadTs } : {}) });
87
+ try {
88
+ await onMessage({
89
+ channel: "slack",
90
+ identityId: identity.id,
91
+ peerId,
92
+ text: filtered.textRaw.trim(),
93
+ raw: event,
94
+ });
95
+ }
96
+ catch (error) {
97
+ log.error({ error, peerId }, "slack inbound handler failed");
98
+ }
99
+ });
100
+ socket.on("app_mention", async (args) => {
101
+ const ack = args?.ack;
102
+ await safeAck(ack);
103
+ const event = args?.event;
104
+ if (!event || typeof event !== "object")
105
+ return;
106
+ const filtered = shouldIgnore(event);
107
+ if (filtered.ok)
108
+ return;
109
+ const threadTs = typeof event.thread_ts === "string" ? event.thread_ts : null;
110
+ const ts = typeof event.ts === "string" ? event.ts : null;
111
+ const rootThread = threadTs || ts;
112
+ const peerId = formatSlackPeerId({ channelId: filtered.channelId, ...(rootThread ? { threadTs: rootThread } : {}) });
113
+ const text = stripSlackMention(filtered.textRaw, botUserId);
114
+ if (!text)
115
+ return;
116
+ try {
117
+ await onMessage({
118
+ channel: "slack",
119
+ identityId: identity.id,
120
+ peerId,
121
+ text,
122
+ raw: event,
123
+ });
124
+ }
125
+ catch (error) {
126
+ log.error({ error, peerId }, "slack inbound handler failed");
127
+ }
128
+ });
129
+ return {
130
+ name: "slack",
131
+ identityId: identity.id,
132
+ maxTextLength: MAX_TEXT_LENGTH,
133
+ async start() {
134
+ if (started)
135
+ return;
136
+ log.debug("slack adapter starting");
137
+ const auth = await web.auth.test();
138
+ botUserId = typeof auth?.user_id === "string" ? auth.user_id : null;
139
+ await socket.start();
140
+ started = true;
141
+ log.info({ botUserId }, "slack adapter started");
142
+ },
143
+ async stop() {
144
+ if (!started)
145
+ return;
146
+ started = false;
147
+ try {
148
+ // socket-mode client uses a websocket; disconnect when stopping.
149
+ await socket.disconnect();
150
+ }
151
+ catch (error) {
152
+ log.warn({ error }, "slack adapter stop failed");
153
+ }
154
+ log.info("slack adapter stopped");
155
+ },
156
+ async sendText(peerId, text) {
157
+ const peer = parseSlackPeerId(peerId);
158
+ if (!peer.channelId)
159
+ throw new Error("Invalid Slack peerId");
160
+ const payload = {
161
+ channel: peer.channelId,
162
+ text,
163
+ };
164
+ if (peer.threadTs)
165
+ payload.thread_ts = peer.threadTs;
166
+ await web.chat.postMessage(payload);
167
+ },
168
+ };
169
+ }
@@ -0,0 +1,78 @@
1
+ import { Bot } from "grammy";
2
+ const MAX_TEXT_LENGTH = 4096;
3
+ export function createTelegramAdapter(identity, config, logger, onMessage) {
4
+ const token = identity.token?.trim() ?? "";
5
+ if (!token) {
6
+ throw new Error("Telegram token is required for Telegram adapter");
7
+ }
8
+ const log = logger.child({ channel: "telegram", identityId: identity.id });
9
+ log.debug({ tokenPresent: true }, "telegram adapter init");
10
+ const bot = new Bot(token);
11
+ bot.catch((err) => {
12
+ log.error({ error: err.error }, "telegram bot error");
13
+ });
14
+ bot.on("message", async (ctx) => {
15
+ const msg = ctx.message;
16
+ if (!msg?.chat)
17
+ return;
18
+ const chatType = msg.chat.type;
19
+ const isGroup = chatType === "group" || chatType === "supergroup" || chatType === "channel";
20
+ // In groups, check if groups are enabled
21
+ if (isGroup && !config.groupsEnabled) {
22
+ log.debug({ chatId: msg.chat.id, chatType }, "telegram message ignored (groups disabled)");
23
+ return;
24
+ }
25
+ let text = msg.text ?? msg.caption ?? "";
26
+ if (!text.trim())
27
+ return;
28
+ // In groups, only respond if the bot is @mentioned
29
+ if (isGroup) {
30
+ const botUsername = ctx.me?.username;
31
+ if (!botUsername) {
32
+ log.debug({ chatId: msg.chat.id }, "telegram message ignored (bot username unknown)");
33
+ return;
34
+ }
35
+ const mentionPattern = new RegExp(`@${botUsername}\\b`, "i");
36
+ if (!mentionPattern.test(text)) {
37
+ log.debug({ chatId: msg.chat.id, botUsername }, "telegram message ignored (not mentioned)");
38
+ return;
39
+ }
40
+ // Strip the @mention from the message
41
+ text = text.replace(mentionPattern, "").trim();
42
+ if (!text) {
43
+ log.debug({ chatId: msg.chat.id }, "telegram message ignored (empty after removing mention)");
44
+ return;
45
+ }
46
+ }
47
+ log.debug({ chatId: msg.chat.id, chatType, isGroup, length: text.length, preview: text.slice(0, 120) }, "telegram message received");
48
+ try {
49
+ await onMessage({
50
+ channel: "telegram",
51
+ identityId: identity.id,
52
+ peerId: String(msg.chat.id),
53
+ text,
54
+ raw: msg,
55
+ });
56
+ }
57
+ catch (error) {
58
+ log.error({ error, peerId: msg.chat.id }, "telegram inbound handler failed");
59
+ }
60
+ });
61
+ return {
62
+ name: "telegram",
63
+ identityId: identity.id,
64
+ maxTextLength: MAX_TEXT_LENGTH,
65
+ async start() {
66
+ log.debug("telegram adapter starting");
67
+ await bot.start();
68
+ log.info("telegram adapter started");
69
+ },
70
+ async stop() {
71
+ bot.stop();
72
+ log.info("telegram adapter stopped");
73
+ },
74
+ async sendText(peerId, text) {
75
+ await bot.api.sendMessage(Number(peerId), text);
76
+ },
77
+ };
78
+ }
package/dist/text.js ADDED
@@ -0,0 +1,41 @@
1
+ export function chunkText(input, limit) {
2
+ if (input.length <= limit)
3
+ return [input];
4
+ const chunks = [];
5
+ let current = "";
6
+ for (const line of input.split(/\n/)) {
7
+ if ((current + line).length + 1 > limit) {
8
+ if (current)
9
+ chunks.push(current.trimEnd());
10
+ current = "";
11
+ }
12
+ if (line.length > limit) {
13
+ for (let i = 0; i < line.length; i += limit) {
14
+ const slice = line.slice(i, i + limit);
15
+ if (slice.length)
16
+ chunks.push(slice);
17
+ }
18
+ continue;
19
+ }
20
+ current += current ? `\n${line}` : line;
21
+ }
22
+ if (current.trim().length)
23
+ chunks.push(current.trimEnd());
24
+ return chunks.length ? chunks : [input];
25
+ }
26
+ export function truncateText(text, limit) {
27
+ if (text.length <= limit)
28
+ return text;
29
+ return `${text.slice(0, Math.max(0, limit - 1))}…`;
30
+ }
31
+ export function formatInputSummary(input) {
32
+ const entries = Object.entries(input);
33
+ if (!entries.length)
34
+ return "";
35
+ try {
36
+ return JSON.stringify(input);
37
+ }
38
+ catch {
39
+ return entries.map(([key, value]) => `${key}=${String(value)}`).join(", ");
40
+ }
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-router",
3
- "version": "0.11.77",
3
+ "version": "0.11.79",
4
4
  "description": "opencode-router: Slack + Telegram bridge + directory routing for a running opencode server",
5
5
  "private": false,
6
6
  "type": "module",