From c2f31551e613a183b624d3c50bffec12c50e0e1a Mon Sep 17 00:00:00 2001 From: 4ellendger <4ellendger@gmail.com> Date: Fri, 20 Feb 2026 00:20:27 +0400 Subject: [PATCH 1/4] feat(client): add markChannelRead method --- telegram-client.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/telegram-client.js b/telegram-client.js index 2bc9cd5..46aa584 100644 --- a/telegram-client.js +++ b/telegram-client.js @@ -546,6 +546,8 @@ class TelegramClient { chatType, isForum, isGroup, + unreadCount: typeof dialog.unreadCount === 'number' ? dialog.unreadCount : 0, + unreadMentionsCount: typeof dialog.unreadMentionsCount === 'number' ? dialog.unreadMentionsCount : 0, }); if (results.length >= effectiveLimit) { @@ -621,6 +623,8 @@ class TelegramClient { chatType, isForum, isGroup, + unreadCount: typeof dialog.unreadCount === 'number' ? dialog.unreadCount : 0, + unreadMentionsCount: typeof dialog.unreadMentionsCount === 'number' ? dialog.unreadMentionsCount : 0, }); } @@ -973,6 +977,18 @@ class TelegramClient { return true; } + async markChannelRead(channelId, messageId) { + await this.ensureLogin(); + const msgId = Number(messageId); + if (!Number.isInteger(msgId) || msgId <= 0) { + throw new Error('messageId must be a positive integer'); + } + const peerRef = normalizeChannelId(channelId); + const peer = await this.client.resolvePeer(peerRef); + await this.client.readHistory(peer, { maxId: msgId }); + return { channelId: peer?.id?.toString?.() ?? String(channelId), messageId: msgId }; + } + async getPeerMetadata(channelId, peerType) { await this.ensureLogin(); const peerRef = normalizeChannelId(channelId); From 6e088e78142a057f23138e84b8c4b098c611239b Mon Sep 17 00:00:00 2001 From: 4ellendger <4ellendger@gmail.com> Date: Fri, 20 Feb 2026 00:20:52 +0400 Subject: [PATCH 2/4] feat(mcp): add markChannelRead tool --- mcp-server.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/mcp-server.js b/mcp-server.js index f8894f6..91696d4 100644 --- a/mcp-server.js +++ b/mcp-server.js @@ -268,6 +268,15 @@ const channelIdSchema = z.union([ z.string({ invalid_type_error: "channelId must be a string" }).min(1), ]); +const markChannelReadSchema = { + channelId: channelIdSchema.describe("Numeric channel ID or username"), + messageId: z + .number({ invalid_type_error: "messageId must be a number" }) + .int() + .positive() + .describe("Mark as read up to this message ID (inclusive)"), +}; + const userIdSchema = z.union([ z.number({ invalid_type_error: "userId must be a number" }), z.string({ invalid_type_error: "userId must be a string" }).min(1), @@ -623,7 +632,7 @@ function createServerInstance() { server.tool( "listChannels", - "Lists available Telegram dialogs for the authenticated account.", + "Lists available Telegram dialogs for the authenticated account, including unread message counts.", listChannelsSchema, async ({ limit }) => { await telegramClient.ensureLogin(); @@ -1694,6 +1703,25 @@ function createServerInstance() { }, ); + server.tool( + "markChannelRead", + "Marks a Telegram channel as read up to the specified message ID.", + markChannelReadSchema, + async ({ channelId, messageId }) => { + await telegramClient.ensureLogin(); + const result = await telegramClient.markChannelRead(channelId, messageId); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + ); + return server; } From 77b7b81b2b0a06fa931cde6db56865085367595b Mon Sep 17 00:00:00 2001 From: 4ellendger <4ellendger@gmail.com> Date: Fri, 20 Feb 2026 00:23:01 +0400 Subject: [PATCH 3/4] feat(cli): add channels mark-read command --- cli.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/cli.js b/cli.js index ab80939..d861b2a 100755 --- a/cli.js +++ b/cli.js @@ -167,6 +167,12 @@ function buildProgram() { .option('--enable', 'Enable sync') .option('--disable', 'Disable sync') .action(withGlobalOptions((globalFlags, options) => runChannelsSync(globalFlags, options))); + channels + .command('mark-read') + .description('Mark channel as read up to a message') + .option('--chat ', 'Channel identifier') + .option('--message-id ', 'Mark as read up to this message ID') + .action(withGlobalOptions((globalFlags, options) => runChannelsMarkRead(globalFlags, options))); const messages = program.command('messages').description('List and search messages'); messages @@ -2058,6 +2064,40 @@ async function runChannelsSync(globalFlags, options = {}) { }, timeoutMs); } +async function runChannelsMarkRead(globalFlags, options = {}) { + const timeoutMs = globalFlags.timeoutMs; + return runWithTimeout(async () => { + if (!options.chat) { + throw new Error('--chat is required'); + } + if (!options.messageId) { + throw new Error('--message-id is required'); + } + const messageId = parsePositiveInt(options.messageId, '--message-id'); + if (messageId === null) { + throw new Error('--message-id must be a positive integer'); + } + const storeDir = resolveStoreDir(); + const release = acquireStoreLock(storeDir); + const { telegramClient, messageSyncService } = createServices({ storeDir }); + try { + if (!(await telegramClient.isAuthorized().catch(() => false))) { + throw new Error('Not authenticated. Run `tgcli auth` first.'); + } + const result = await telegramClient.markChannelRead(options.chat, messageId); + if (globalFlags.json) { + writeJson(result); + } else { + console.log(`Marked channel ${result.channelId} as read up to message ${result.messageId}.`); + } + } finally { + await messageSyncService.shutdown(); + await telegramClient.destroy(); + release(); + } + }, timeoutMs); +} + function createLiveMetadataResolver(messageSyncService, telegramClient) { return async (channelId, fallback = {}) => { const meta = messageSyncService.getChannelMetadata(channelId); From a25a7e1562ca42e5345e108351d02e8be64a6e74 Mon Sep 17 00:00:00 2001 From: 4ellendger <4ellendger@gmail.com> Date: Mon, 2 Mar 2026 00:50:45 +0400 Subject: [PATCH 4/4] feat(messages): add --offset-id option for pagination Adds --offset-id CLI flag and corresponding offsetId support in TelegramClient.getMessagesByChannelId(), allowing callers to fetch messages older than a given message ID. Useful for paginating through large channels without re-fetching already-seen messages. --- cli.js | 8 +++++++- telegram-client.js | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cli.js b/cli.js index d861b2a..d004dda 100755 --- a/cli.js +++ b/cli.js @@ -184,6 +184,7 @@ function buildProgram() { .option('--after ', 'Filter messages after date') .option('--before ', 'Filter messages before date') .option('--limit ', 'Limit results') + .option('--offset-id ', 'Fetch messages older than this message ID (for pagination)') .action(withGlobalOptions((globalFlags, options) => runMessagesList(globalFlags, options))); messages .command('search') @@ -2151,7 +2152,12 @@ async function runMessagesList(globalFlags, options = {}) { const response = await telegramClient.getTopicMessages(id, topicId, finalLimit); liveMessages = response.messages; } else { - const response = await telegramClient.getMessagesByChannelId(id, finalLimit); + const fetchOptions = {}; + const offsetId = parsePositiveInt(options.offsetId, '--offset-id'); + if (offsetId) { + fetchOptions.offsetId = offsetId; + } + const response = await telegramClient.getMessagesByChannelId(id, finalLimit, fetchOptions); liveMessages = response.messages; peerTitle = response.peerTitle ?? null; } diff --git a/telegram-client.js b/telegram-client.js index 46aa584..4ab4b48 100644 --- a/telegram-client.js +++ b/telegram-client.js @@ -643,6 +643,7 @@ class TelegramClient { minId = 0, maxId = 0, reverse = false, + offsetId = 0, } = options; const peerRef = normalizeChannelId(channelId); const peer = await this.client.resolvePeer(peerRef); @@ -664,6 +665,11 @@ class TelegramClient { iterOptions.maxId = maxId; } + if (offsetId) { + iterOptions.offset = { id: offsetId, date: 0 }; + iterOptions.addOffset = 0; + } + for await (const message of this.client.iterHistory(peer, iterOptions)) { messages.push(this._serializeMessage(message, peer)); if (messages.length >= effectiveLimit) {