@ghostpaw/telegram 0.6.0
Your bot remembers the chat, including what was edited.
telegram writes every incoming update into a SQLite file before your handler runs: messages, edits, replies, reactions, members and files. Your bot and your agent read from that record, and the agent's tools return plans until you switch execution on.
As a library
As a local MCP server
Replay the run Read the source on GitHub
Free and MIT licensed for Node.js 24 or newer, with zero runtime dependencies. The mirror is a SQLite file on your disk; the bot token stays in your environment.

0runtime dependencies
One group, three updates, six calls
Your handler never has to save anything. The row is already there.
This ran with createBot and long polling against telegram 0.6.0 on 2026-10-24. The Telegram servers were replaced by a local stand-in passed through the documented api.fetch option, so three updates arrive in a group called Renewal desk and nothing reaches Telegram. Everything else is the library. Run the steps in order.
Mirror
- Messages stored0
- Edits kept0
- Outbound calls0
- Last read—
- Tool result—
Eight empty tables after initTelegramTables(db).
-
01 Lena posts in the group
The first update arrives through long polling.
const bot = createBot({ token, db, api: { fetch: standIn }, onMessage: async ({ message }) => { read.getMessage(db, message.chatId, message.messageId); // already there? }, }); await bot.start();// update 1, chat -1001234 "Renewal desk", from Lena (@lena_ops) // "Invoice 4471 is paid. The café receipt is in the shared drive." // inside onMessage: read.getMessage(...) -> row found // { messageId: 10, direction: 'in', fromDisplayName: 'Lena', type: 'text', … }The row was written before the handler ran, so the handler can rely on it.
-
02 The handler replies
One plain-text reply from inside onMessage.
await write.sendMessage(bot.client, -1001234, 'Noted. Filed under 4471.');// the stand-in received: // POST sendMessage { chat_id: -1001234, text: 'Noted. Filed under 4471.' }One outbound call. With a real token this goes to api.telegram.org.
-
03 Lena edits her message
An
edited_messageupdate, one minute later.// update 2: edited_message, msg 10 read.getMessage(db, -1001234, 10).text; read.editHistory(db, -1001234, 10);// "Invoice 4471 is paid. The café receipt is in the finance folder." // [{ id: 1, messageId: 10, // text: 'Invoice 4471 is paid. The café receipt is in the shared drive.' }]The message shows the new text, and the old one is kept. You can see what changed and when.
-
04 Jun replies to it
A reply to message 10.
// update 3: "Which café?", reply_to_message: 10 read.replyChain(db, -1001234, 11);// [{ messageId: 10, text: 'Invoice 4471 is paid. The café receipt is in the finance folder.' }, // { messageId: 11, text: 'Which café?' }]The chain runs from the original down to the reply.
-
05 Search without the accent
FTS5 over the mirror, no network.
read.searchMessages(db, -1001234, 'cafe');// [{ messageId: 11, snippet: 'Which <b>café</b>?' }, // { messageId: 10, snippet: 'Invoice 4471 is paid. The <b>café</b> receipt is in the shared drive.' }]Both messages, accent or not. The snippet for message 10 still shows the text from before the edit; see the note below.
-
06 The agent wants to answer
The
tg_sendtool, called the way an agent harness calls it.tools.getTelegramToolByName('tg_send').handler(db, { subcommand: 'send_message', chatId: -1001234, text: 'Receipt found, thanks.', });// { ok: true, outcome: 'success', summary: 'Message ready to send', // data: { action: 'send_message', chatId: -1001234, text: 'Receipt found, thanks.' } }A plan, and the outbound count is still one. Sending takes
createTelegramToolFacade({ client, execute: true }), which you have to write yourself.
In this run the search index kept the text from before the edit: a search for “finance” found nothing and “shared” still found message 10. editHistory and getMessage were right. We do not claim that search sees edits.
The README says files are downloaded eagerly. In 0.6.0 DEFAULTS.autoDownload is false, so check that setting if you want file bytes stored.
What the mirror keeps, and four tools to read it
Eight tables, created in one call. The agent reads them through four tools.
- Messages
- Text, media, edits, reply edges, albums and threads, with an FTS5 index that folds diacritics.
- Users, chats, members
- Names, chat titles and types, member status, permissions and admin titles.
- Reactions, callbacks
- Emoji per user per message, and inline button presses with their data.
- Files
- Metadata, and the bytes as a BLOB deduplicated by SHA-256 when downloading is on.
tg_read- Messages, chats, users, reactions, edit history and reply chains.
tg_send- Send, edit, delete, forward and pin, set reactions, broadcast.
tg_manage- Ban, unban, restrict, promote and kick, and member status.
tg_connect- Bot stats, bot state and active chats.
Each tool carries its JSON Schema, a side-effect level and a readOnly flag, so a harness can tell reading from acting without reading docs.
If a bot that posts by itself worries you
Some people log every agent action to a Telegram bot as an audit trail. That trail is only useful if you know what the bot did on its own.
- The tools return plans by default. Execution needs an explicit facade with
execute: true. - The executing facade limits calls per chat and globally with token buckets, waits on Telegram's
retry_afterand retries twice by default. It masks bot tokens in errors. - The MCP server executes sends only when
TELEGRAM_BOT_TOKENis set, keeps the mirror in~/.ghostpaw/telegram.dband does not poll for updates itself. - The library does not filter senders for you. In 0.6.0
DEFAULTS.allowedChatIdsexists but nothing reads it, so the allowlist belongs in your handler. - Node.js 24 or newer, because it uses the built-in
node:sqliteand global fetch.
What it costs to add
- Runtime dependencies
- 0
- Node.js
- 24 or newer (node:sqlite)
- Size of dist/index.js, gzip
- 33.7 KB
- Import time
- about 11.8 ms
- Tests
- 883 passing
- Line coverage
- 97.9%
- Agent tools
- 4
Measured 2026-10-24 on the maintainer's machine with Node.js 24.14.0 against telegram 0.6.0: size of the published dist/index.js at gzip -9, median of 11 cold imports, tests and coverage from the v0.6.0 tag.
Start from the README
import { DatabaseSync } from 'node:sqlite';
import { createBot, initTelegramTables } from '@ghostpaw/telegram';
const db = new DatabaseSync('bot.db');
initTelegramTables(db);
const bot = createBot({
token: process.env.TELEGRAM_TOKEN,
db,
onMessage: async ({ message, user }) => {
console.log(user?.firstName, message.text);
},
});
await bot.start(); // long-polling
To give an agent the mirror as an MCP server, add npx -y @ghostpaw/telegram mcp to your client.
Read the READMEtelegram on npm
All seven GhostPaw librariescodex, for memoryquestlog, for tasksaffinity, for contactssouls, for identitygrimoire, for skillsemail, for mail