@ironcode-ai/telegram 1.16.1
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/README.md +97 -0
- package/package.json +24 -0
- package/src/index.ts +344 -0
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# @ironcode-ai/telegram
|
|
2
|
+
|
|
3
|
+
Telegram bot integration for ironcode. Send messages from Telegram to run an AI coding agent on your machine — tool calls, file edits, and responses stream back in real-time.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- [Bun](https://bun.sh) >= 1.0
|
|
8
|
+
- [`ironcode-ai`](https://www.npmjs.com/package/ironcode-ai) CLI installed and authenticated
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
### 1. Install ironcode-ai CLI
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun install -g ironcode-ai
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### 2. Authenticate a provider
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
ironcode auth login # GitHub Copilot (recommended)
|
|
22
|
+
ironcode auth login anthropic # Anthropic
|
|
23
|
+
ironcode auth login openai # OpenAI
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
List available models:
|
|
27
|
+
```bash
|
|
28
|
+
ironcode models
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 3. Create a Telegram Bot
|
|
32
|
+
|
|
33
|
+
1. Open Telegram → search for [@BotFather](https://t.me/BotFather)
|
|
34
|
+
2. Send `/newbot` → follow prompts → copy the **Bot Token**
|
|
35
|
+
|
|
36
|
+
### 4. Install ironcode-telegram
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bun install -g @ironcode-ai/telegram
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 5. Configure
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
ironcode-telegram setup
|
|
46
|
+
# Bot Token (from @BotFather): ****
|
|
47
|
+
# Model [github-copilot/claude-sonnet-4.6]:
|
|
48
|
+
# ✅ Config saved to ~/.config/ironcode/telegram.json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Config is stored at `~/.config/ironcode/telegram.json`:
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"token": "your-bot-token",
|
|
55
|
+
"model": "github-copilot/claude-sonnet-4.6"
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 6. Run
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# cd into the repo you want the agent to work on
|
|
63
|
+
cd /path/to/your/project
|
|
64
|
+
|
|
65
|
+
ironcode-telegram
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The agent runs with the current directory as its working directory, so it can read and edit files in your project.
|
|
69
|
+
|
|
70
|
+
## Bot Commands
|
|
71
|
+
|
|
72
|
+
| Command | Description |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `/sessions` | List recent sessions — tap to switch |
|
|
75
|
+
| `/new` | Start a new session |
|
|
76
|
+
| `/info` | Show current session details (title, ID, file changes) |
|
|
77
|
+
| `/start` | Show help |
|
|
78
|
+
|
|
79
|
+
## How It Works
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
You send a message
|
|
83
|
+
→ Bot creates/resumes an ironcode session on your machine
|
|
84
|
+
→ Agent reads/writes files, runs bash, calls LLM
|
|
85
|
+
→ Each completed tool call is sent as a separate message
|
|
86
|
+
→ Text response is streamed live by editing a ⏳ placeholder
|
|
87
|
+
→ Final response replaces the placeholder when done
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Each chat (DM, group, or group thread) gets its own independent session. Use `/sessions` to switch between them.
|
|
91
|
+
|
|
92
|
+
## Session Management
|
|
93
|
+
|
|
94
|
+
- **Automatic**: Each chat automatically gets its own session on first message.
|
|
95
|
+
- **Switch**: `/sessions` shows the 10 most recent sessions with an inline keyboard. Tap any to resume it.
|
|
96
|
+
- **New**: `/new` clears the current slot — your next message starts a fresh session.
|
|
97
|
+
- **Persistent**: Sessions and their file change history are preserved across bot restarts.
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ironcode-ai/telegram",
|
|
3
|
+
"version": "1.16.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ironcode-telegram": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "bun run src/index.ts",
|
|
14
|
+
"typecheck": "tsgo --noEmit"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@ironcode-ai/sdk": "^1.15.4",
|
|
18
|
+
"grammy": "^1.36.3"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.13.9",
|
|
22
|
+
"typescript": "^5.9.3"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Bot, InlineKeyboard } from "grammy"
|
|
3
|
+
import { createIroncode } from "@ironcode-ai/sdk"
|
|
4
|
+
import type { Session } from "@ironcode-ai/sdk"
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs"
|
|
6
|
+
import { join } from "path"
|
|
7
|
+
import { homedir } from "os"
|
|
8
|
+
import * as readline from "readline"
|
|
9
|
+
|
|
10
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
type Config = {
|
|
13
|
+
token: string
|
|
14
|
+
model?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function configPath() {
|
|
18
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config")
|
|
19
|
+
return join(xdg, "ironcode", "telegram.json")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function loadConfig(): Config | null {
|
|
23
|
+
const p = configPath()
|
|
24
|
+
if (!existsSync(p)) return null
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(p, "utf8")) as Config
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function saveConfig(cfg: Config) {
|
|
33
|
+
const p = configPath()
|
|
34
|
+
mkdirSync(join(p, ".."), { recursive: true })
|
|
35
|
+
writeFileSync(p, JSON.stringify(cfg, null, 2))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function ask(prompt: string): Promise<string> {
|
|
39
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
rl.question(prompt, (ans) => {
|
|
42
|
+
rl.close()
|
|
43
|
+
resolve(ans.trim())
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Setup command ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
if (process.argv[2] === "setup") {
|
|
51
|
+
console.log("⚙️ ironcode-telegram setup\n")
|
|
52
|
+
|
|
53
|
+
const existing = loadConfig()
|
|
54
|
+
const token = await ask(`Bot Token (from @BotFather)${existing?.token ? " [keep current: Enter]" : ""}: `)
|
|
55
|
+
const model = await ask(`Model [${existing?.model ?? "github-copilot/claude-sonnet-4.6"}]: `)
|
|
56
|
+
|
|
57
|
+
const cfg: Config = {
|
|
58
|
+
token: token || existing?.token || "",
|
|
59
|
+
model: model || existing?.model || "github-copilot/claude-sonnet-4.6",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!cfg.token) {
|
|
63
|
+
console.error("❌ Bot Token is required.")
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
saveConfig(cfg)
|
|
68
|
+
console.log(`\n✅ Config saved to ${configPath()}`)
|
|
69
|
+
console.log(" Run: ironcode-telegram\n")
|
|
70
|
+
process.exit(0)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Load config ───────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
const cfg = loadConfig()
|
|
76
|
+
if (!cfg) {
|
|
77
|
+
console.error(`❌ No config found. Run:\n\n ironcode-telegram setup\n`)
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
function parseModel(model: string) {
|
|
84
|
+
const [providerID, ...rest] = model.split("/")
|
|
85
|
+
return { providerID, modelID: rest.join("/") }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function relativeTime(ms: number) {
|
|
89
|
+
const diff = Date.now() - ms
|
|
90
|
+
const m = Math.floor(diff / 60000)
|
|
91
|
+
const h = Math.floor(diff / 3600000)
|
|
92
|
+
const d = Math.floor(diff / 86400000)
|
|
93
|
+
if (m < 1) return "just now"
|
|
94
|
+
if (m < 60) return `${m}m ago`
|
|
95
|
+
if (h < 24) return `${h}h ago`
|
|
96
|
+
return `${d}d ago`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sessionLabel(s: Session, isCurrent: boolean) {
|
|
100
|
+
const changes = s.summary ? ` (+${s.summary.additions}/-${s.summary.deletions})` : ""
|
|
101
|
+
const cur = isCurrent ? " ✓" : ""
|
|
102
|
+
return `${s.title}${changes} · ${relativeTime(s.time.updated)}${cur}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Bot ───────────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
const bot = new Bot(cfg.token)
|
|
108
|
+
|
|
109
|
+
console.log("🚀 Starting ironcode server...")
|
|
110
|
+
const ironcode = await createIroncode({ port: 0 })
|
|
111
|
+
console.log("✅ Ironcode server ready at", ironcode.server.url)
|
|
112
|
+
|
|
113
|
+
type SessionState = {
|
|
114
|
+
sessionId: string
|
|
115
|
+
chatId: number
|
|
116
|
+
threadId?: number
|
|
117
|
+
liveMessageId?: number
|
|
118
|
+
liveText: string
|
|
119
|
+
lastEditMs: number
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const EDIT_INTERVAL_MS = 1200
|
|
123
|
+
|
|
124
|
+
const sessions = new Map<string, SessionState>()
|
|
125
|
+
|
|
126
|
+
function getChatKey(chatId: number, threadId?: number) {
|
|
127
|
+
return threadId ? `${chatId}-${threadId}` : `${chatId}`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function editLive(state: SessionState, text: string) {
|
|
131
|
+
if (!state.liveMessageId) return
|
|
132
|
+
await bot.api
|
|
133
|
+
.editMessageText(state.chatId, state.liveMessageId, text || "…")
|
|
134
|
+
.catch(() => {})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Event loop
|
|
138
|
+
;(async () => {
|
|
139
|
+
const events = await ironcode.client.event.subscribe()
|
|
140
|
+
for await (const event of events.stream) {
|
|
141
|
+
const getState = (sessionID: string) =>
|
|
142
|
+
[...sessions.values()].find((s) => s.sessionId === sessionID)
|
|
143
|
+
|
|
144
|
+
if (event.type === "message.part.updated") {
|
|
145
|
+
const part = event.properties.part as any
|
|
146
|
+
const state = getState(part.sessionID)
|
|
147
|
+
if (!state) continue
|
|
148
|
+
|
|
149
|
+
if (part.type === "text") {
|
|
150
|
+
state.liveText = part.text
|
|
151
|
+
const now = Date.now()
|
|
152
|
+
if (state.liveMessageId && now - state.lastEditMs > EDIT_INTERVAL_MS) {
|
|
153
|
+
await editLive(state, state.liveText)
|
|
154
|
+
state.lastEditMs = now
|
|
155
|
+
}
|
|
156
|
+
} else if (part.type === "tool" && part.state?.status === "completed") {
|
|
157
|
+
await bot.api
|
|
158
|
+
.sendMessage(state.chatId, `*${part.tool}* — ${part.state.title}`, {
|
|
159
|
+
parse_mode: "Markdown",
|
|
160
|
+
...(state.threadId ? { message_thread_id: state.threadId } : {}),
|
|
161
|
+
})
|
|
162
|
+
.catch(() => {})
|
|
163
|
+
}
|
|
164
|
+
} else if (event.type === "message.updated") {
|
|
165
|
+
const info = event.properties.info as any
|
|
166
|
+
if (info.role !== "assistant") continue
|
|
167
|
+
const state = getState(info.sessionID)
|
|
168
|
+
if (!state) continue
|
|
169
|
+
|
|
170
|
+
if (info.error) {
|
|
171
|
+
const msg = info.error.data?.message ?? info.error.name ?? "Unknown error"
|
|
172
|
+
await editLive(state, `❌ ${msg}`)
|
|
173
|
+
state.liveMessageId = undefined
|
|
174
|
+
state.liveText = ""
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (info.finish && info.finish !== "tool-calls" && info.finish !== "unknown") {
|
|
179
|
+
const finalText = state.liveText.trim()
|
|
180
|
+
if (finalText) {
|
|
181
|
+
await editLive(state, finalText)
|
|
182
|
+
} else if (state.liveMessageId) {
|
|
183
|
+
await bot.api.deleteMessage(state.chatId, state.liveMessageId).catch(() => {})
|
|
184
|
+
}
|
|
185
|
+
state.liveMessageId = undefined
|
|
186
|
+
state.liveText = ""
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
})()
|
|
191
|
+
|
|
192
|
+
bot.catch((err) => {
|
|
193
|
+
console.error("❌ Unhandled bot error:", err.message)
|
|
194
|
+
err.ctx.reply(`❌ ${err.message}`).catch(() => {})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
bot.command("start", async (ctx) => {
|
|
200
|
+
await ctx.reply(
|
|
201
|
+
"👋 *IronCode Bot*\n\n" +
|
|
202
|
+
"Send a message to start coding with the AI agent.\n\n" +
|
|
203
|
+
"Commands:\n" +
|
|
204
|
+
"/sessions — list sessions\n" +
|
|
205
|
+
"/new — start a new session\n" +
|
|
206
|
+
"/info — current session details",
|
|
207
|
+
{ parse_mode: "Markdown" },
|
|
208
|
+
)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
bot.command("new", async (ctx) => {
|
|
212
|
+
const key = getChatKey(ctx.chat.id, ctx.message?.message_thread_id)
|
|
213
|
+
sessions.delete(key)
|
|
214
|
+
await ctx.reply("✨ New session will be created on your next message.")
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
bot.command("info", async (ctx) => {
|
|
218
|
+
const key = getChatKey(ctx.chat.id, ctx.message?.message_thread_id)
|
|
219
|
+
const state = sessions.get(key)
|
|
220
|
+
|
|
221
|
+
if (!state) {
|
|
222
|
+
await ctx.reply("No active session. Send a message to create one.")
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const res = await ironcode.client.session.get({ path: { id: state.sessionId } })
|
|
227
|
+
if (res.error) {
|
|
228
|
+
await ctx.reply(`❌ ${JSON.stringify(res.error)}`)
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const s = res.data
|
|
233
|
+
const changes = s.summary
|
|
234
|
+
? `📊 ${s.summary.files} files · +${s.summary.additions}/-${s.summary.deletions}`
|
|
235
|
+
: "📊 No changes yet"
|
|
236
|
+
|
|
237
|
+
await ctx.reply(
|
|
238
|
+
`*${s.title}*\n` +
|
|
239
|
+
`ID: \`${s.id}\`\n` +
|
|
240
|
+
`Created: ${relativeTime(s.time.created)}\n` +
|
|
241
|
+
`Updated: ${relativeTime(s.time.updated)}\n` +
|
|
242
|
+
changes,
|
|
243
|
+
{ parse_mode: "Markdown" },
|
|
244
|
+
)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
bot.command("sessions", async (ctx) => {
|
|
248
|
+
const key = getChatKey(ctx.chat.id, ctx.message?.message_thread_id)
|
|
249
|
+
const currentState = sessions.get(key)
|
|
250
|
+
|
|
251
|
+
const res = await ironcode.client.session.list()
|
|
252
|
+
if (res.error) {
|
|
253
|
+
await ctx.reply(`❌ ${JSON.stringify(res.error)}`)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const list = res.data!
|
|
258
|
+
.filter((s) => !(s as any).time?.archived)
|
|
259
|
+
.sort((a, b) => b.time.updated - a.time.updated)
|
|
260
|
+
.slice(0, 10)
|
|
261
|
+
|
|
262
|
+
if (list.length === 0) {
|
|
263
|
+
await ctx.reply("No sessions yet.")
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const kb = new InlineKeyboard()
|
|
268
|
+
for (const s of list) {
|
|
269
|
+
kb.text(sessionLabel(s, s.id === currentState?.sessionId), `switch:${s.id}`).row()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await ctx.reply("Select a session to continue:", { reply_markup: kb })
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
bot.callbackQuery(/^switch:(.+)$/, async (ctx) => {
|
|
276
|
+
const sessionId = ctx.match[1]
|
|
277
|
+
const chatId = ctx.chat!.id
|
|
278
|
+
const threadId = (ctx.callbackQuery.message as any)?.message_thread_id
|
|
279
|
+
const key = getChatKey(chatId, threadId)
|
|
280
|
+
|
|
281
|
+
const res = await ironcode.client.session.get({ path: { id: sessionId } })
|
|
282
|
+
if (res.error) {
|
|
283
|
+
await ctx.answerCallbackQuery({ text: "❌ Session not found" })
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const existing = sessions.get(key)
|
|
288
|
+
sessions.set(key, { sessionId, chatId, threadId, liveText: "", lastEditMs: 0, liveMessageId: existing?.liveMessageId })
|
|
289
|
+
|
|
290
|
+
await ctx.answerCallbackQuery({ text: "✓ Session switched" })
|
|
291
|
+
await ctx.editMessageText(`✅ Now using: *${res.data!.title}*`, { parse_mode: "Markdown" })
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
// ── Main message handler ──────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
bot.on("message:text", async (ctx) => {
|
|
297
|
+
const chatId = ctx.chat.id
|
|
298
|
+
const threadId = ctx.message.message_thread_id
|
|
299
|
+
const key = getChatKey(chatId, threadId)
|
|
300
|
+
const text = ctx.message.text
|
|
301
|
+
|
|
302
|
+
if (text.startsWith("/")) return
|
|
303
|
+
|
|
304
|
+
let state = sessions.get(key)
|
|
305
|
+
|
|
306
|
+
if (!state) {
|
|
307
|
+
const res = await ironcode.client.session.create({
|
|
308
|
+
body: { title: `Telegram ${ctx.chat.type} ${key}` },
|
|
309
|
+
})
|
|
310
|
+
if (res.error) {
|
|
311
|
+
await ctx.reply(`❌ Failed to create session: ${JSON.stringify(res.error)}`)
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
state = { sessionId: res.data.id, chatId, threadId, liveText: "", lastEditMs: 0 }
|
|
315
|
+
sessions.set(key, state)
|
|
316
|
+
|
|
317
|
+
const share = await ironcode.client.session.share({ path: { id: res.data.id } })
|
|
318
|
+
if (!share.error && share.data?.share?.url) {
|
|
319
|
+
await ctx.reply(`🔗 Session: ${share.data.share.url}`)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const placeholder = await ctx.reply("⏳", {
|
|
324
|
+
...(threadId ? { message_thread_id: threadId } : {}),
|
|
325
|
+
})
|
|
326
|
+
state.liveMessageId = placeholder.message_id
|
|
327
|
+
state.liveText = ""
|
|
328
|
+
state.lastEditMs = 0
|
|
329
|
+
|
|
330
|
+
const model = cfg.model ? parseModel(cfg.model) : undefined
|
|
331
|
+
|
|
332
|
+
const result = await ironcode.client.session.promptAsync({
|
|
333
|
+
path: { id: state.sessionId },
|
|
334
|
+
body: { parts: [{ type: "text", text }], model },
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
if (result.error) {
|
|
338
|
+
await ctx.api.editMessageText(chatId, placeholder.message_id, `❌ ${JSON.stringify(result.error)}`)
|
|
339
|
+
state.liveMessageId = undefined
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
await bot.start()
|
|
344
|
+
console.log("⚡️ Telegram bot is running!")
|