@pedrohnas/opencode-telegram 1.2.1 → 1.3.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 (53) hide show
  1. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-41-55-194Z.yml +36 -0
  2. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-42-17-115Z.yml +36 -0
  3. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-43-15-988Z.yml +26 -0
  4. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-43-26-107Z.yml +26 -0
  5. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-45-03-139Z.yml +29 -0
  6. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-45-21-579Z.yml +29 -0
  7. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-45-48-051Z.yml +30 -0
  8. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-46-27-632Z.yml +33 -0
  9. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-46-46-519Z.yml +33 -0
  10. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-47-28-491Z.yml +349 -0
  11. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-47-34-834Z.yml +349 -0
  12. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-47-54-066Z.yml +168 -0
  13. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-48-19-667Z.yml +219 -0
  14. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-49-32-311Z.yml +221 -0
  15. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-49-57-109Z.yml +230 -0
  16. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-50-24-052Z.yml +235 -0
  17. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-50-41-148Z.yml +248 -0
  18. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-51-10-916Z.yml +234 -0
  19. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-51-28-271Z.yml +234 -0
  20. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-52-32-324Z.yml +234 -0
  21. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-52-47-801Z.yml +196 -0
  22. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-56-07-361Z.yml +203 -0
  23. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-56-35-534Z.yml +49 -0
  24. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-57-04-658Z.yml +52 -0
  25. package/docs/AUDIT.md +193 -0
  26. package/docs/PROGRESS.md +188 -0
  27. package/docs/plans/phase-5.md +410 -0
  28. package/docs/plans/phase-6.5.md +426 -0
  29. package/docs/plans/phase-6.md +349 -0
  30. package/e2e/helpers.ts +34 -0
  31. package/e2e/phase-5.test.ts +295 -0
  32. package/e2e/phase-6.5.test.ts +239 -0
  33. package/e2e/phase-6.test.ts +302 -0
  34. package/package.json +5 -2
  35. package/src/api-server.test.ts +309 -0
  36. package/src/api-server.ts +201 -0
  37. package/src/bot.test.ts +354 -0
  38. package/src/bot.ts +200 -2
  39. package/src/config.test.ts +16 -0
  40. package/src/config.ts +4 -0
  41. package/src/event-bus.test.ts +337 -1
  42. package/src/event-bus.ts +83 -3
  43. package/src/handlers/agents.test.ts +122 -0
  44. package/src/handlers/agents.ts +93 -0
  45. package/src/handlers/media.test.ts +264 -0
  46. package/src/handlers/media.ts +168 -0
  47. package/src/handlers/models.test.ts +319 -0
  48. package/src/handlers/models.ts +191 -0
  49. package/src/index.ts +15 -0
  50. package/src/send/draft-stream.test.ts +76 -0
  51. package/src/send/draft-stream.ts +13 -1
  52. package/src/session-manager.test.ts +46 -0
  53. package/src/session-manager.ts +10 -1
@@ -0,0 +1,239 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test"
2
+ import { setup, teardown, getClient, getBotUsername } from "./runner"
3
+ import {
4
+ sendAndWait,
5
+ assertHasButtons,
6
+ clickAndWaitEdit,
7
+ } from "./helpers"
8
+ import { Api } from "telegram/tl"
9
+
10
+ const API_BASE = "http://127.0.0.1:4097"
11
+
12
+ describe("Phase 6.5 — Production Hardening + Bot Control API", () => {
13
+ beforeAll(async () => {
14
+ await setup()
15
+ }, 90000)
16
+
17
+ afterAll(async () => {
18
+ await teardown()
19
+ })
20
+
21
+ // --- /model filtering ---
22
+
23
+ test(
24
+ "/model shows filtered providers (small count, not 86)",
25
+ async () => {
26
+ const client = getClient()
27
+ const bot = getBotUsername()
28
+ const reply = await sendAndWait(client, bot, "/model", 15000)
29
+ assertHasButtons(reply)
30
+
31
+ const markup = reply.replyMarkup as Api.ReplyInlineMarkup
32
+ const providerCount = markup.rows.length
33
+ // Should show only connected providers (typically 4-5, never 86)
34
+ expect(providerCount).toBeGreaterThan(0)
35
+ expect(providerCount).toBeLessThan(10)
36
+
37
+ // Each button should show model count in parentheses
38
+ const firstText = markup.rows[0].buttons[0].text
39
+ expect(firstText).toMatch(/\(\d+\)/)
40
+ },
41
+ 30000,
42
+ )
43
+
44
+ test(
45
+ "clicking provider shows filtered models (small count per provider)",
46
+ async () => {
47
+ const client = getClient()
48
+ const bot = getBotUsername()
49
+
50
+ const providerMsg = await sendAndWait(client, bot, "/model", 15000)
51
+ const markup = providerMsg.replyMarkup as Api.ReplyInlineMarkup
52
+ const firstButton = markup.rows[0].buttons[0]
53
+
54
+ const modelMsg = await clickAndWaitEdit(
55
+ client,
56
+ bot,
57
+ providerMsg.id,
58
+ firstButton.text,
59
+ )
60
+
61
+ const modelMarkup = modelMsg.replyMarkup as Api.ReplyInlineMarkup
62
+ // Filtered models: should be small number (e.g. 3-5 per provider, not 22+)
63
+ // Subtract 1 for Back button
64
+ const modelCount = modelMarkup.rows.length - 1
65
+ expect(modelCount).toBeGreaterThan(0)
66
+ expect(modelCount).toBeLessThan(15)
67
+ },
68
+ 30000,
69
+ )
70
+
71
+ // --- Bot Control API ---
72
+
73
+ test(
74
+ "GET /api/sessions returns at least 1 session after sending a message",
75
+ async () => {
76
+ const client = getClient()
77
+ const bot = getBotUsername()
78
+
79
+ // Ensure we have a session by sending a message
80
+ await sendAndWait(client, bot, "/new", 30000)
81
+ await sendAndWait(
82
+ client,
83
+ bot,
84
+ "Respond with exactly the single word: hi",
85
+ 60000,
86
+ )
87
+
88
+ // Now call the Bot Control API
89
+ const res = await fetch(`${API_BASE}/api/sessions`)
90
+ expect(res.status).toBe(200)
91
+ const sessions = await res.json() as any[]
92
+ expect(sessions.length).toBeGreaterThanOrEqual(1)
93
+
94
+ // Each session should have expected fields
95
+ const session = sessions[0]
96
+ expect(session.chatId).toBeDefined()
97
+ expect(session.sessionId).toBeDefined()
98
+ },
99
+ 120000,
100
+ )
101
+
102
+ test(
103
+ "POST /api/session/:id/model changes the model override",
104
+ async () => {
105
+ // Get a sessionId from the API
106
+ const sessRes = await fetch(`${API_BASE}/api/sessions`)
107
+ const sessions = await sessRes.json() as any[]
108
+ expect(sessions.length).toBeGreaterThan(0)
109
+ const sessionId = sessions[0].sessionId
110
+
111
+ // Get available models to find a valid providerID/modelID
112
+ const modelsRes = await fetch(`${API_BASE}/api/models`)
113
+ const providers = await modelsRes.json() as any[]
114
+ expect(providers.length).toBeGreaterThan(0)
115
+ const provider = providers[0]
116
+ expect(provider.models.length).toBeGreaterThan(0)
117
+ const model = provider.models[0]
118
+
119
+ // Set model override
120
+ const setRes = await fetch(`${API_BASE}/api/session/${sessionId}/model`, {
121
+ method: "POST",
122
+ headers: { "Content-Type": "application/json" },
123
+ body: JSON.stringify({ providerID: provider.id, modelID: model.id }),
124
+ })
125
+ expect(setRes.status).toBe(200)
126
+
127
+ // Verify via GET
128
+ const getRes = await fetch(`${API_BASE}/api/session/${sessionId}`)
129
+ const data = await getRes.json() as any
130
+ expect(data.model).toEqual({ providerID: provider.id, modelID: model.id })
131
+ },
132
+ 30000,
133
+ )
134
+
135
+ test(
136
+ "POST /api/session/:id/agent changes the agent override",
137
+ async () => {
138
+ const sessRes = await fetch(`${API_BASE}/api/sessions`)
139
+ const sessions = await sessRes.json() as any[]
140
+ const sessionId = sessions[0].sessionId
141
+
142
+ // Get available agents
143
+ const agentsRes = await fetch(`${API_BASE}/api/agents`)
144
+ const agents = await agentsRes.json() as any[]
145
+ expect(agents.length).toBeGreaterThan(0)
146
+ const agentName = agents[0].name
147
+
148
+ // Set agent override
149
+ const setRes = await fetch(`${API_BASE}/api/session/${sessionId}/agent`, {
150
+ method: "POST",
151
+ headers: { "Content-Type": "application/json" },
152
+ body: JSON.stringify({ agent: agentName }),
153
+ })
154
+ expect(setRes.status).toBe(200)
155
+
156
+ // Verify via GET
157
+ const getRes = await fetch(`${API_BASE}/api/session/${sessionId}`)
158
+ const data = await getRes.json() as any
159
+ expect(data.agent).toBe(agentName)
160
+ },
161
+ 30000,
162
+ )
163
+
164
+ test(
165
+ "POST /api/session/:id/new creates a new session",
166
+ async () => {
167
+ const sessRes = await fetch(`${API_BASE}/api/sessions`)
168
+ const sessions = await sessRes.json() as any[]
169
+ const oldSessionId = sessions[0].sessionId
170
+
171
+ // Create new session
172
+ const newRes = await fetch(`${API_BASE}/api/session/${oldSessionId}/new`, {
173
+ method: "POST",
174
+ })
175
+ expect(newRes.status).toBe(200)
176
+ const data = await newRes.json() as any
177
+ expect(data.sessionId).toBeDefined()
178
+ expect(data.oldSessionId).toBe(oldSessionId)
179
+ expect(data.sessionId).not.toBe(oldSessionId)
180
+ },
181
+ 30000,
182
+ )
183
+
184
+ test(
185
+ "GET /api/models returns providers with filtered models",
186
+ async () => {
187
+ const res = await fetch(`${API_BASE}/api/models`)
188
+ expect(res.status).toBe(200)
189
+ const providers = await res.json() as any[]
190
+ expect(providers.length).toBeGreaterThan(0)
191
+
192
+ // Each provider should have models
193
+ for (const p of providers) {
194
+ expect(p.id).toBeDefined()
195
+ expect(p.name).toBeDefined()
196
+ expect(p.models.length).toBeGreaterThan(0)
197
+ // Each model should have expected fields
198
+ for (const m of p.models) {
199
+ expect(m.id).toBeDefined()
200
+ expect(m.name).toBeDefined()
201
+ }
202
+ }
203
+ },
204
+ 15000,
205
+ )
206
+
207
+ test(
208
+ "GET /api/agents returns agents",
209
+ async () => {
210
+ const res = await fetch(`${API_BASE}/api/agents`)
211
+ expect(res.status).toBe(200)
212
+ const agents = await res.json() as any[]
213
+ expect(agents.length).toBeGreaterThan(0)
214
+ expect(agents[0].name).toBeDefined()
215
+ },
216
+ 15000,
217
+ )
218
+
219
+ // --- regression ---
220
+
221
+ test(
222
+ "regression: text message works after API model/agent changes",
223
+ async () => {
224
+ const client = getClient()
225
+ const bot = getBotUsername()
226
+
227
+ const reply = await sendAndWait(
228
+ client,
229
+ bot,
230
+ "Respond with exactly the single word: pong",
231
+ 60000,
232
+ )
233
+ expect(reply).toBeDefined()
234
+ const text = reply.text ?? reply.message ?? ""
235
+ expect(text.length).toBeGreaterThan(0)
236
+ },
237
+ 120000,
238
+ )
239
+ })
@@ -0,0 +1,302 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test"
2
+ import { setup, teardown, getClient, getBotUsername } from "./runner"
3
+ import { sendAndWait } from "./helpers"
4
+ import { CustomFile } from "telegram/client/uploads"
5
+ import { deflateSync } from "node:zlib"
6
+
7
+ /**
8
+ * Helper: send a file to the bot and wait for a reply.
9
+ * Uses gramjs client.sendFile() to upload and then polls for bot response.
10
+ */
11
+ async function sendFileAndWait(
12
+ client: ReturnType<typeof getClient>,
13
+ botUsername: string,
14
+ params: {
15
+ file: Buffer
16
+ filename: string
17
+ caption?: string
18
+ forceDocument?: boolean
19
+ },
20
+ timeoutMs = 60000,
21
+ ) {
22
+ // Get the latest message ID before sending
23
+ const messagesBefore = await client.getMessages(botUsername, { limit: 1 })
24
+ const lastIdBefore = messagesBefore[0]?.id ?? 0
25
+
26
+ await client.sendFile(botUsername, {
27
+ file: new CustomFile(params.filename, params.file.length, "", params.file),
28
+ caption: params.caption,
29
+ forceDocument: params.forceDocument,
30
+ })
31
+
32
+ // Poll for bot reply
33
+ const deadline = Date.now() + timeoutMs
34
+ while (Date.now() < deadline) {
35
+ await new Promise((r) => setTimeout(r, 1500))
36
+ const messages = await client.getMessages(botUsername, { limit: 5 })
37
+ for (const msg of messages) {
38
+ if (!msg.out && msg.id > lastIdBefore) {
39
+ return msg
40
+ }
41
+ }
42
+ }
43
+ throw new Error(`Bot did not reply within ${timeoutMs}ms after sending file`)
44
+ }
45
+
46
+ describe("Phase 6 — Media & Files", () => {
47
+ beforeAll(async () => {
48
+ await setup()
49
+ // Start fresh session
50
+ const client = getClient()
51
+ const bot = getBotUsername()
52
+ await sendAndWait(client, bot, "/new", 30000)
53
+ }, 90000)
54
+
55
+ afterAll(async () => {
56
+ await teardown()
57
+ })
58
+
59
+ test(
60
+ "sending photo gets AI response about the image",
61
+ async () => {
62
+ const client = getClient()
63
+ const bot = getBotUsername()
64
+
65
+ // Create a valid 10x10 red PNG using zlib.deflateSync
66
+ const pngBuffer = createValidPng()
67
+
68
+ const reply = await sendFileAndWait(client, bot, {
69
+ file: pngBuffer,
70
+ filename: "test-image.png",
71
+ caption: "What do you see in this image? Respond briefly.",
72
+ })
73
+
74
+ expect(reply).toBeDefined()
75
+ const text = reply.text ?? reply.message ?? ""
76
+ expect(text.length).toBeGreaterThan(0)
77
+ },
78
+ 120000,
79
+ )
80
+
81
+ test(
82
+ "sending document gets AI response",
83
+ async () => {
84
+ const client = getClient()
85
+ const bot = getBotUsername()
86
+
87
+ // Send a small text file as document
88
+ const docBuffer = Buffer.from("Hello, this is a test document.\nIt has two lines.")
89
+
90
+ const reply = await sendFileAndWait(client, bot, {
91
+ file: docBuffer,
92
+ filename: "test-doc.txt",
93
+ caption: "What does this file contain? Respond briefly.",
94
+ forceDocument: true,
95
+ })
96
+
97
+ expect(reply).toBeDefined()
98
+ const text = reply.text ?? reply.message ?? ""
99
+ expect(text.length).toBeGreaterThan(0)
100
+ },
101
+ 120000,
102
+ )
103
+
104
+ test(
105
+ "photo with caption includes caption in response context",
106
+ async () => {
107
+ const client = getClient()
108
+ const bot = getBotUsername()
109
+
110
+ const pngBuffer = createValidPng()
111
+
112
+ const reply = await sendFileAndWait(client, bot, {
113
+ file: pngBuffer,
114
+ filename: "captioned.png",
115
+ caption: "Describe the color of this image in one word.",
116
+ })
117
+
118
+ expect(reply).toBeDefined()
119
+ const text = reply.text ?? reply.message ?? ""
120
+ expect(text.length).toBeGreaterThan(0)
121
+ },
122
+ 120000,
123
+ )
124
+
125
+ test(
126
+ "regression: text message still works after media",
127
+ async () => {
128
+ const client = getClient()
129
+ const bot = getBotUsername()
130
+ await sendAndWait(client, bot, "/new", 30000)
131
+
132
+ const reply = await sendAndWait(
133
+ client,
134
+ bot,
135
+ "Respond with exactly the single word: pong",
136
+ 60000,
137
+ )
138
+ expect(reply).toBeDefined()
139
+ const text = reply.text ?? reply.message ?? ""
140
+ expect(text.length).toBeGreaterThan(0)
141
+ },
142
+ 120000,
143
+ )
144
+
145
+ test(
146
+ "regression: no message fragmentation after media then text",
147
+ async () => {
148
+ const client = getClient()
149
+ const bot = getBotUsername()
150
+ await sendAndWait(client, bot, "/new", 30000)
151
+
152
+ // 1. Send a photo and wait for response
153
+ const pngBuffer = createValidPng()
154
+ const mediaReply = await sendFileAndWait(client, bot, {
155
+ file: pngBuffer,
156
+ filename: "test.png",
157
+ caption: "Describe this briefly in one sentence.",
158
+ })
159
+ expect(mediaReply).toBeDefined()
160
+ const mediaReplyId = mediaReply.id
161
+
162
+ // 2. Wait a moment for streaming to fully finish
163
+ await new Promise((r) => setTimeout(r, 3000))
164
+
165
+ // 3. Send text message and wait for response
166
+ const textReply = await sendAndWait(
167
+ client,
168
+ bot,
169
+ "Respond with exactly the single word: pong",
170
+ 90000,
171
+ )
172
+ expect(textReply).toBeDefined()
173
+ const textReplyId = textReply.id
174
+
175
+ // 4. Count bot messages between media reply and text reply
176
+ // If streaming is fragmented, there will be many messages in between
177
+ const messages = await client.getMessages(bot, { limit: 20 })
178
+ const botMessages = messages.filter(
179
+ (m) => !m.out && m.id > mediaReplyId && m.id <= textReplyId,
180
+ )
181
+
182
+ // Should be exactly 1 message (the text reply).
183
+ // If fragmented, there would be many intermediate messages.
184
+ expect(botMessages.length).toBeLessThanOrEqual(2) // Allow 1 for text reply + maybe 1 for tool status
185
+ },
186
+ 180000,
187
+ )
188
+
189
+ test(
190
+ "regression: sending text while AI responds to media doesn't fragment",
191
+ async () => {
192
+ const client = getClient()
193
+ const bot = getBotUsername()
194
+ await sendAndWait(client, bot, "/new", 30000)
195
+
196
+ // 1. Send a photo (triggers AI response)
197
+ const pngBuffer = createValidPng()
198
+ const messagesBefore = await client.getMessages(bot, { limit: 1 })
199
+ const lastIdBefore = messagesBefore[0]?.id ?? 0
200
+
201
+ await client.sendFile(bot, {
202
+ file: new CustomFile("interrupt.png", pngBuffer.length, "", pngBuffer),
203
+ caption: "Write a 3 paragraph essay about colors.",
204
+ })
205
+
206
+ // 2. Wait briefly for streaming to start, then interrupt with text
207
+ await new Promise((r) => setTimeout(r, 4000))
208
+
209
+ const reply = await sendAndWait(
210
+ client,
211
+ bot,
212
+ "Respond with exactly the single word: pong",
213
+ 90000,
214
+ )
215
+
216
+ expect(reply).toBeDefined()
217
+ const text = reply.text ?? reply.message ?? ""
218
+ expect(text.length).toBeGreaterThan(0)
219
+
220
+ // 3. Count ALL bot messages after our initial send
221
+ await new Promise((r) => setTimeout(r, 3000))
222
+ const allMessages = await client.getMessages(bot, { limit: 30 })
223
+ const botMessagesAfter = allMessages.filter(
224
+ (m) => !m.out && m.id > lastIdBefore,
225
+ )
226
+
227
+ // Should have at most 3 bot messages:
228
+ // 1. Draft from photo response (may be edited/finalized)
229
+ // 2. Possibly aborted/partial photo response
230
+ // 3. The "pong" text reply
231
+ // Fragmentation would cause 5+ messages
232
+ expect(botMessagesAfter.length).toBeLessThanOrEqual(4)
233
+ },
234
+ 180000,
235
+ )
236
+ })
237
+
238
+ /**
239
+ * Create a valid 10x10 red PNG image using Node's zlib for correct deflate.
240
+ * Telegram requires properly encoded images — hand-crafted zlib is fragile.
241
+ */
242
+ function createValidPng(): Buffer {
243
+ const width = 10
244
+ const height = 10
245
+ const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
246
+
247
+ // IHDR: 10x10, 8-bit RGB
248
+ const ihdrData = Buffer.alloc(13)
249
+ ihdrData.writeUInt32BE(width, 0)
250
+ ihdrData.writeUInt32BE(height, 4)
251
+ ihdrData[8] = 8 // bit depth
252
+ ihdrData[9] = 2 // color type (RGB)
253
+ ihdrData[10] = 0 // compression
254
+ ihdrData[11] = 0 // filter
255
+ ihdrData[12] = 0 // interlace
256
+ const ihdr = createPngChunk("IHDR", ihdrData)
257
+
258
+ // Raw image data: each row starts with filter byte 0, then RGB pixels
259
+ const rawRows: Buffer[] = []
260
+ for (let y = 0; y < height; y++) {
261
+ const row = Buffer.alloc(1 + width * 3) // filter byte + RGB per pixel
262
+ row[0] = 0 // filter: None
263
+ for (let x = 0; x < width; x++) {
264
+ row[1 + x * 3] = 0xff // R
265
+ row[1 + x * 3 + 1] = 0x00 // G
266
+ row[1 + x * 3 + 2] = 0x00 // B
267
+ }
268
+ rawRows.push(row)
269
+ }
270
+ const rawImageData = Buffer.concat(rawRows)
271
+ const compressedData = deflateSync(rawImageData)
272
+ const idat = createPngChunk("IDAT", compressedData)
273
+
274
+ // IEND
275
+ const iend = createPngChunk("IEND", Buffer.alloc(0))
276
+
277
+ return Buffer.concat([signature, ihdr, idat, iend])
278
+ }
279
+
280
+ function createPngChunk(type: string, data: Buffer): Buffer {
281
+ const typeBytes = Buffer.from(type, "ascii")
282
+ const length = Buffer.alloc(4)
283
+ length.writeUInt32BE(data.length, 0)
284
+ const combined = Buffer.concat([typeBytes, data])
285
+
286
+ const crc = crc32(combined)
287
+ const crcBuf = Buffer.alloc(4)
288
+ crcBuf.writeUInt32BE(crc >>> 0, 0)
289
+
290
+ return Buffer.concat([length, combined, crcBuf])
291
+ }
292
+
293
+ function crc32(data: Buffer): number {
294
+ let crc = ~0
295
+ for (let i = 0; i < data.length; i++) {
296
+ crc ^= data[i]!
297
+ for (let j = 0; j < 8; j++) {
298
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0)
299
+ }
300
+ }
301
+ return ~crc
302
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pedrohnas/opencode-telegram",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -10,8 +10,11 @@
10
10
  "typecheck": "tsgo --noEmit"
11
11
  },
12
12
  "dependencies": {
13
+ "@grammyjs/runner": "2.0.3",
14
+ "@grammyjs/transformer-throttler": "1.2.1",
13
15
  "@opencode-ai/sdk": "^1.1.53",
14
- "grammy": "^1.35.0"
16
+ "grammy": "^1.35.0",
17
+ "hono": "4.11.9"
15
18
  },
16
19
  "devDependencies": {
17
20
  "@types/bun": "^1.2.0",