telegram-download-chat
AGENTS.md222 starsChanged 24 days agoMITView on GitHub
Telegram Download Chat is a Python CLI utility that downloads and analyzes Telegram chat history. It provides both command-line and GUI interfaces for downloading messages from chats, groups, channels, or archived exports and saving them in JSON/TXT formats. The application follows a modular design: 1. Configuration Layer: YAML-based config with API credentials and user settings 2. Telegram Client Layer: Telethon wrapper for authenticated API communication 3. Processing Layer: Message filtering, date splitting, format conversion 4. Interface Layer: CLI and GUI…
- Installs packages
- Commits and pushes
We split this AGENTS.md into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 27
Use .venv virtual environment.
- Very specific: line 54
After export/render changes, save live e2e results to the repo's ./data directory (gitignored.
- Distinctive: line 89
If a step fails and you re-run, skip the already-completed steps.
≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Telegram Download Chat is a Python CLI utility that downloads and analyzes Telegram chat history. It provides both command-line and GUI interfaces for downloading messages from chats, groups, channels, or archived exports and saving them in JSON/TXT formats.
Key Components
- Core Engine (
core/package): ContainsTelegramChatDownloaderplus helper modules (auth,config,download,entities,media,messages,context,render,progress,comments,citations,reactions,update_checker) built on Telethon - CLI Interface (
cli.py): Command-line interface with argument parsing and async message processing - GUI Interface (
gui_app.py): PySide6-based graphical interface with threading for async operations - MCP Server (
mcp/package): Model Context Protocol server exposing Telegram chat tools for AI assistants - Configuration (
paths.py): Handles config file management and application directories
Architecture
The application follows a modular design:
- Configuration Layer: YAML-based config with API credentials and user settings
- Telegram Client Layer: Telethon wrapper for authenticated API communication
- Processing Layer: Message filtering, date splitting, format conversion
- Interface Layer: CLI and GUI frontends sharing the same core functionality
Development Commands
Use .venv virtual environment.
Setup Development Environment
# Install in development mode with all dependencies
pip install -e ".[dev,gui]"
# Or install from requirements
pip install -r requirements.txtTesting
# Run tests
pytest
# Run tests with async support
pytest -v
# Run specific test
pytest tests/test_telegram_download_chat.py::TestClass::test_method
# Opt-in end-to-end export tests against a live Telegram group (skipped by default).
# Requires an authenticated session and membership in the test group.
TG_E2E=1 pytest -m e2e # override the group via TG_E2E_GROUPAfter export/render changes, save live e2e results to the repo's ./data directory
(gitignored; the dir is kept via data/.gitkeep) rather than a path outside the repo.
Code Quality
# Format code
black src/ tests/
# Sort imports
isort src/ tests/
# Type checking
mypy src/Building
# Build package
python -m build
# Install from source
pip install .
# Build PyInstaller executables
./build_macos.sh # macOS
./build_windows.ps1 # WindowsRelease
# Bump version, run tests, build (bumpversion commits and tags)
python deploy.py patch # or: minor | major
git push && git push --tagsdeploy.pydoes not upload to PyPI. TheBuild and Releaseworkflow owns publishing: it triggers on thev*tag, builds the Windows artifacts, uploads to PyPI and creates the GitHub release. Uploading from both made the workflow's publish job fail with a 400 from PyPI, since the files were already there.- If a step fails and you re-run, skip the already-completed steps.
- To fold release fixups into the version-bump commit, amend it (
git commit --amend).
Running
# CLI mode
python -m telegram_download_chat username
# GUI mode
python -m telegram_download_chat gui
# or
telegram-download-chat gui
# From source
python main.py # Launches GUI by defaultConfiguration
- Config file auto-created at OS-specific locations (see
paths.py) - Requires Telegram API credentials from https://my.telegram.org
- Example config in
config.example.yml - Supports optional proxy via
proxy_urlin config or--proxy-urlCLI flag (socks5/socks4/http) - GUI provides config editing interface
Key Features to Understand
Message Processing
- Downloads via Telethon's
iter_messages()with pagination - Supports resume from interruption using temporary files
- Can filter by date ranges, specific users, or message threads
- Outputs JSON (full metadata), TXT (human-readable), and optionally HTML/PDF formats
- Output is organized per-chat:
<chat_name>/messages.json,<chat_name>/messages.txt, optionallymessages.html/messages.pdf, and<chat_name>/attachments/
Authentication
- Uses Telethon sessions for persistent login
- Only
telegram-download-chat loginmay prompt.loginsigns in with a code (Telethon's interactivestart()),login --qrscans a QR code. Handled incli/__init__.py: main()bysys.argv[1], likegui; the flow lives incli/login.py. loginowns every prompt (_prompt_phone/_prompt_code/_prompt_password, the last two passed to Telethon'sstart()ascode_callback/passwordso its owninput()default never runs after a code was already sent). They refuse to run without a tty and raiseNoTerminalErrorwith a one-line instruction, becausegetpasswithout a terminal either echoes the password or dies with a bareEOFError— which is what a QR login hit once the scan was accepted and 2FA kicked in. Failures printstr(e) or type(e).__name__, so a message-less exception is still named.cli/login.py: ensure_session()runs before a download (async_main, skipped when every target is a local.json) and is the one place a download may still log in: it prompts only whensys.stdinis a tty (_can_prompt), offering[Y]es / [q]r / [n]o, and otherwise just logsNO_SESSION_MESSAGEand fails with exit code 1. The GUI's subprocess (stdin=DEVNULL) and the console-less Windows build (sys.stdin is None) therefore never reach a prompt. On success the client is left connected and authorized, soconnect()short-circuits on itsis_user_authorized()check instead of initializing a second client on the same session file. The interactive body is shared withrun_login(_log_in/_report_login_error), andEOFError/KeyboardInterruptat a prompt is reported as "Login cancelled."- Unusable credentials raise
ConfigurationError(aValueErrorsubclass,core/auth_utils.py) naming my.telegram.org, including theYOUR_API_IDplaceholders a first run writes —int('YOUR_API_ID')used to crash the second run with a traceback printed over the first-run instructions.prepare_client()logs the message and the CLI only sets the exit code, so it is never printed twice. core/auth.py: connect()never prompts: a missing session raisesNoSessionError(core/auth_utils.py) naming bothloginand the GUI. The GUI runs downloads as a CLI subprocess with no stdin, where Telethon'sinput()dies withRuntimeError: lost sys.stdin(issue #91). Client setup lives inprepare_client(), whichloginuses to reach an unauthorized client. A code request needs an explicitly passed phone — a phone left in the config no longer turns a download into a silent code request.- A second attempt after a scan was already accepted never shows a code: Telegram issues no new token while the session waits for the 2FA password, so
client.qr_login()itself raisesSessionPasswordNeededErrorandqr_login()goes straight to signing in with the password (found on Windows in the GUI, where the hint therefore says to press the button again rather than to rescan). - A wrong 2FA password is re-asked
DEFAULT_PASSWORD_ATTEMPTS(3) times in the CLI and once in the GUI (which hands over a field value it cannot re-read), then fails withQrLoginError— Telethon'sstart()has the same retry for code logins, andPasswordHashInvalidError's own wording ("the password and thus its hash value") is not shown.QrLoginTimeoutsubclassesQrLoginError, andcli/login.pyprints everyQrLoginErroras a plain line instead of a traceback. core/qr_login.py:qr_login()refreshes the login token (~30 s expiry) until the code is scanned, raisesQrLoginTimeoutafter the overall deadline, and signs in with a password onSessionPasswordNeededError.render_qr_ascii/render_qr_ansi(CLI; ANSI pins black-on-white so a dark terminal cannot invert the code) andqr_code_matrix(GUI pixmap) share theqrcodedependency — which must stay in bothpyproject.tomlandrequirements.txt, since the Windows embeddable build installs from the latter.- GUI login: phone/code/password flow plus a "Log in with QR code" button (
gui/tabs/settings_tab.py,gui/auth/session_manager.py, dialog ingui/widgets/qr_dialog.py). The QR flow is split by thread deliberately:login_qrreads widgets on the UI thread,_do_qr_login_asyncdoes Telethon work only,finish_qr_loginapplies the outcome back on the UI thread. The loop and task are created before the worker thread starts so Cancel can stop a pending login instead of holding the session file. 2FA takes the password from the Password field (a worker thread cannot prompt). - GUI QR + 2FA: a worker thread cannot prompt, so
SessionPasswordNeededErroris turned into "Enter your password in the Password field and scan the QR code again". - GUI download gate:
MainWindow._logged_intracks the session (None= not known yet, which still allows the download) and refuses to start one when logged out.SettingsTab._set_logged_instoresis_logged_inand emitsauth_state_changed; the window also readsis_logged_indirectly, because the tab validates the session while it is being constructed, before the signal is connected. __main__.pyexits withmain()'s return code (sys.exit(main())). It used to drop it, sopython -m telegram_download_chat— which is exactly how the GUI starts a download — exited 0 on every failure and made the exit-code reporting below useless. The console script always propagated it, so a CLI run looked fine and only the GUI lied.cli/__init__.pyalso swallowsNoSessionError's traceback:connect()has already logged the instruction, and a stack trace on top of it only hides it.WorkerThread.finishedcarries the subprocess exit code, and a non-zero one is reported as a failure — it used to print a traceback into the log while the status bar said "Download completed".TelegramAuth.initializewraps the client'ssend_code_request(_trace_send_code_request), so both Telethon's interactivestart()(CLI) and the GUI flow log the masked phone, theSentCodedelivery type (app / SMS / Firebase / email setup …),next_type, timeout and DC — at DEBUG only (--debug), so a "code never arrives" report can be diagnosed from the log.log_filein config resolves relative paths against the app dir (not the cwd); default is<app dir>/app.log.
Filtering & Splitting
--subchat: Extract message threads/replies--split: Split output by month/year--user: Filter by specific sender--max-date: Messages on or before this date--min-date: Messages on or after this date--media-placeholders: Insert media type indicators (e.g.[photo],[file=name.pdf]) in TXT output--media: Download all media types with organized category directories (images/, videos/, documents/, audio/, stickers/, contacts/, locations/, polls/, etc.) and concurrent downloads (5 simultaneous). Supports photos, videos, documents, audio, stickers, contacts (VCF), geo locations (JSON), polls, dice, and games. Files above ~5 MB use parallel multi-connection MTProto chunking (FastTelethon-style, seecore/fast_download.py); connection count auto-tunes per Premium status (Premium=4, free=2) and is overridable viamedia_parallel_connectionsin config. Threshold is overridable viamedia_parallel_threshold_mb. Earlier defaults (8/4 connections, 1 MB threshold) triggered Telegram's per-account throttling and stalled the run. During long throttled runs, Telegram file references can expire before a file is reached; these are automatically refetched (by message id) and the download is retried once via the standard single-stream downloader.--no-fast-download: Disable the parallel chunked downloader and fall back to single-stream Telethon for all files.
Channel Comments
--comments(broadcast-channel only): resolves the channel's linked discussion supergroup (GetFullChannelRequest→linked_chat_id) and fetches its comments in a single date-bounded pass rather than one request per post.fetch_discussion_messagespages the linked discussion group once viaclient.iter_messages(entity), bounded by the in-window posts' earliest date (_earliest_post_date) — a comment is always newer than its post, so the pass stops once it goes older.map_discussion_to_commentsthen maps each discussion message to its parent channel post by thread root: forwarded root messages carryfwd_from.channel_post(the original channel post id), buildingroot_to_post, and each comment resolves its post viareply_to_top_id or reply_to_msg_id. This replaces the old per-postiter_messages(channel_entity, reply_to=post_id)scan, turning ~one request per post into a few dozen for the whole channel. Each comment is normalized so the existing render logic nests it under its post:reply_to.reply_to_msg_idand the top-levelreply_to_msg_idare set to the channel post id,comment_of=<post_id>is added, and the native discussion id is preserved asdiscussion_msg_id. Comments mapping to out-of-window posts (root never seen) are skipped with a debug log. Comments are appended into the samemessages.json(then deduped). Because comments live in a separate id space, comment records (those carryingcomment_of) are excluded from the post-based resume cursor and_dedup_messageskeys them by(comment_of, id)to avoid collisions with channel post ids. On a resume collision, a freshly-fetched comment carryingattachment_pathreplaces a same-key stale comment that lacks one (_has_attachment, mirroring the citation-replace precedent; never demotes a comment that already has a path) — so resume runs finally surface comment media that an earlier export saved without its link.--comments-limit N: caps comments fetched per post (requires--comments; omit/0/negative = unlimited). The GUI exposes this as a "Comments per post" dropdown (No limit / 10 / 50 / 100 / 500 / 1000) beside the "Download channel post comments" checkbox.--comments-min-reactions N: drops comments whose total reaction count (total_reaction_count, sum of all reaction counts) is below N, applied insidemap_discussion_to_commentsafter the per-post--comments-limitcap and before comment media is downloaded (so dropped comments never trigger a media fetch; their raw media messages are excluded by id).0/unset keeps all. Filtered comments never reachmessages.json/.txt/.html.- A
type: "comments"structured progress event (posts done/total, comments so far) is emitted once for the single discussion pass (posts_done == posts_total == len(post_ids)) viacore/progress.pyand surfaced bygui/worker.pyparallel to themediaevent. Comment fetching is implicitly bounded by the in-window posts' date floor. - Comment media: under
--media, comments carrying media are downloaded into the chat'sattachments/dir (reusingdownload_all_media) and each normalized comment dict is stamped withattachment_pathso the saved JSON keeps it and HTML renders it inline. Comment dicts hold serialized (non-Telethon) media, so the post-media pass skips them and there is no double-download. - Comment resume: there is no per-post checkpoint sidecar — the old
<chat>/messages.comments-progress.jsonand its helpers were retired with the single-pass rewrite. A resume simply re-fetches the discussion group once (cheap now that it is a single date-bounded pass) and relies on the standard output-merge plus(comment_of, id)dedup to stay duplicate-free; the Part A attachment-aware replace rule (above) additionally lets a re-fetched comment recover a missingattachment_path.
Citations (outside-window replies)
core/citations.py: after a download,fetch_outside_window_citations(incli/commands.py) collects reply-target ids that are referenced but not present inmessages(collect_missing_cited_ids) and fetches them by id viaget_messages(ids=...)so JSON/TXT/HTML show the quoted citation. Runs unconditionally (covers both date-window and finite---limitcases) and is best-effort — failures are logged and skipped. Comment records (comment_of is not None) are excluded from both the present-set and the missing-set, and fetched posts can't collide with the comment id-space because dedup keys comments by(comment_of, id).
Speech-to-Text (--stt)
core/stt.py: transcribes voice messages and round video notes through Telegram Premium's server-side speech-to-text. Telethon has no high-level wrapper, so it calls the rawfunctions.messages.TranscribeAudioRequest(peer, msg_id)→messages.TranscribedAudio(transcription_id, text, pending, …). The pass is kick-then-collect: phase 1 issues one request per message, phase 2 re-issues only the ones Telegram reportedpending. That order matters — Telegram transcribes in parallel and a real voice message takes 15-30 s, so waiting on each in turn times out on nearly all of them (an earlier per-message 12 s backoff transcribed 0/30 on a live chat). Phase 2 polls everyPENDING_POLL_SECONDS(5 s) up toPENDING_TIMEOUT_SECONDS(180 s); apending=Trueresult normally completes viaUpdateTranscribedAudio, but re-issuing returns the same, now-finished transcription, which keeps this a plain request/response loop.- Addressing — messages are addressed by their own
peer_id, not by the chat identifier the CLI was given. That identifier is a string, and Telethon reads a digit string as a username/phone: resolving it issuesGetContactsRequest, an online lookup that can rate-limit the account for hours and fail the entire pass.peer_idresolves from the session cache; an unknown peer falls back to the identifier once. - Rate limits —
_with_flood_retrywraps both peer resolution and the transcribe request. It catchesFloodError, notFloodWaitError:FloodPremiumWaitErroris its sibling rather than its subclass, and Telegram's burst limit arrives as a bareFLOODwith no retry time. A wait overFLOOD_WAIT_MAX_SECONDS(30 s), or one outlastingFLOOD_RETRY_BACKOFF_SECONDS(5/10/20 s), raises the internal_RateLimitedand ends the pass once instead of being re-hit per message — but phase 2 still runs, because audio already handed to Telegram keeps transcribing. Reporting is human-readable (retry in 2h 17m), names permanent refusals (MSG_VOICE_TOO_LONG→ "longer than Telegram transcribes") at warning level rather than swallowing them at debug, and counts them apart from what the limit actually blocked. is_transcribable(msg)accepts both raw Telethon messages and dicts (resume/convert paths) and matches exactly what Telegram transcribes:DocumentAttributeAudio.voiceorDocumentAttributeVideo.round_message.- Cache —
TranscriptCacheover an append-only JSONL file (get_app_dir()/stt-cache.jsonl, overridable viasettings.stt_cache_path). Keyed by the media document id (transcript_key), which is globally unique and survives forwarding, so the same audio is never transcribed twice — across re-downloads,--overwrite, a different--output, or another chat. Append-only means a crash keeps what was already transcribed and two concurrent runs (e.g. GUI + CLI) cannot clobber each other; on load, later lines win and corrupt lines are skipped. Only successes are cached, so failures/pending retry on the next run. - Order inside the pass (
transcribe_messages): candidates → cache lookups → only then_detect_premium_once()and API calls. Consequences: an account without Premium still gets every cached transcript into the export, and a fully-cached run never touches Premium detection at all. - Comments live in the linked discussion group, so
_resolve_targetaddresses them bydiscussion_msg_idagainst the peer rebuilt from theirpeer_id, not by the exported channel-post id. - Wiring —
--stt(off by default) →cli/commands.py: apply_transcriptions, a best-effort pass run after the--subchat/--keywordsfilters (deliberate: transcription is slow and rate-limited, so filtered-out messages are never transcribed; the trade-off is that--keywordscannot match transcript text). It stashes{document_id: text}ondownloader._transcripts, whichsave_messagesstamps onto each message dict astranscript— Telethon'sto_dict()drops attributes set on a raw message, mirroring thecited_outside_window/_forum_topic_titlesprecedent. - Render — driven by the presence of the
transcriptfield, not by the flag, so converting an existing JSON export renders transcripts too:[stt] <text>line in TXT, a.sttblock under the message in HTML, and an inline italic paragraph in PDF. No structured progress event; progress is INFO log lines everyPROGRESS_LOG_EVERY(10) transcriptions, which the GUI log panel shows. GUI: a "Transcribe voice messages (Telegram Premium)" checkbox in the download tab, persisted likemedia/comments.
Reactions
core/reactions.py:normalize_reactionsconverts TelethonMessageReactionsinto a stable list[{emoji|custom_emoji_id, count, chosen?, recent?}], applied inmessages.py(save_messages) andrender.py. Idempotent on already-normalized input (resume/convert paths).render.pyrenders them as reaction pills (emoji + count,chosenhighlighted; custom emoji show a star placeholder with the document id in the tooltip).total_reaction_count(sum of all counts) andformat_reactions_text("👍5 ❤️2 ⭐3", custom emoji →⭐) are shared helpers incore/reactions.py.--reactionsappendsformat_reactions_textas an inline[…]suffix to each message's TXT line (save_messages_as_txt, off by default);total_reaction_countbacks both--comments-min-reactionsand the HTML comment filter.- HTML comment filter:
render.pystamps each.bblwithdata-reactions(the message'sreactions_total) and, when the page has comments, renders a "top N%" filter bar._comment_reaction_percentiles(totals)precomputes each percentile's reaction threshold + matching count server-side (buttons: All / Top 50% / 20% / 10% / 5%); a small vanilla-JS handler hides comment.bblbelow the chosen threshold live, hides emptied.grp, and updates each collapsible<details>summary count. Filtering is view-only (nothing is removed from the export).
Export Formats
--html: Render a Telegram Web-style HTML page (uses Jinja2 templates). Channel comments render in a collapsible per-post<details>block (collapsed shows the comment count); the redundant parent-post citation inside each comment is suppressed. When comments are present, a sticky "top N%" reaction filter bar lets the reader hide low-reaction comments live (see Reactions).--pdf: Render a PDF document (uses ReportLab). Comments render inline (interleaved by timestamp) since the PDF path cannot collapse them.- Both flags work alongside existing JSON/TXT output and can be combined with
--mediafor inline images
Structured GUI Progress
- The core emits machine-readable progress events (
core/progress.py) as single JSON stdout lines prefixed with@@TDCPROGRESS@@(PROGRESS_PREFIX), gated by theTDC_STRUCTURED_PROGRESSenv var (PROGRESS_ENV_VAR). - The GUI worker sets that env var on the CLI subprocess and parses the lines (
parse_progress_line) into Qt signals instead of scraping log text; normal CLI runs stay clean. Event types:messages,media,media_summary. In-process callers/tests can pass asinkcallable toemit_progress. - GUI stylesheet helpers live in
gui/utils/styles.py(e.g.style_checkboxes) so unchecked checkboxes match the input background.
Update Checker (Windows GUI)
core/update_checker.pyqueries GitHubreleases/latest, parses thevX.Y.Ztag, and compares versions. The installer download URL (telegram-download-chat.exe) is resolved on Windows only; other platforms open the releases page.- Surfaced via the Settings tab "Software Update" group; stale concurrent checks are discarded via a monotonic request-id guard.
PyInstaller Integration
- Custom hooks in
_pyinstaller/for bundling - Platform-specific build scripts
- GUI auto-launches when no CLI args provided
Two-part Windows build (embeddable Python, tiny updates)
build_windows_embed.ps1produces a two-part portable distribution so per-release updates are ~150 KB instead of re-shipping the runtime:- Base (
runtime/): the official Windows embeddable CPython + all third-party packages (pip install --target runtime/site-packages) + launchers (telegram-download-chat.cmdCLI,telegram-download-chat-gui.vbsGUI). ApythonXY._pthpoints the interpreter at..\site-packagesand..\..\app. Installed once; re-shipped only on a Python/dependency bump (the Python minor version is pinned, so deps'.pydare ABI-locked to it). - App (
app/): only ourtelegram_download_chatsource, replaced wholesale on each release.
- Base (
scripts/package_embed.pyis the build-time half (unit-tested intests/test_package_embed.py):build_app_zipmakesapp-<version>.zip(source + generated_version.py+version.txt, no bytecode). CLI:package_embed.py build-app …. There is no manifest/per-file-diff (the oldpackage_portable.pyonedir scheme was removed) — the app part is small enough to replace wholesale.core/app_updater.pyis the runtime half shipped inside the app (unit-tested intests/test_app_updater.py):find_app_install_dir(detects the<install>/app/telegram_download_chat+ siblingruntime/layout),download_app_zip,apply_app_update(atomicapp/swap: optional sha256 verify → extract to temp → validate payload → rename-swap with rollback), andperform_app_update(download + apply, refuses when not an embeddable install). Manual CLI:python -m telegram_download_chat.core.app_updater apply <zip> <install-dir>.- GUI integration (
gui/tabs/settings_tab.py): when an update is available andfind_app_install_dir()is non-None, the "Download" button becomes "Update now" and runsperform_app_update(update_checker.get_app_update_url(latest))on a worker thread, then offers a restart (relaunch viaQProcess.startDetached(sys.executable, ["-m","telegram_download_chat","gui"])). Otherwise it falls back to opening the release/installer URL in the browser.update_checker.get_app_update_url(version)builds theapp-<version>.zipasset URL. - Optional
setup.exe(installer.iss+build_windows_installer.ps1, sanity-tested intests/test_installer_inno.py): Inno Setup wrapper around the portable tree.build_windows_installer.ps1runsbuild_windows_embed.ps1, reads the version fromdist\telegram-download-chat\app\version.txt, and compilesinstaller.iss(/dMyAppVersion=) intodist\telegram-download-chat-v<version>-setup.exe. Install is per-user (PrivilegesRequired=lowest,DefaultDirName={localappdata}\Programs\...) and shortcuts runruntime\python\pythonw.exe -m telegram_download_chat guiwithWorkingDir={app}— both deliberate so the in-app updater can swapapp\without elevation and withoutapp\being the process cwd. The release workflow (.github/workflows/build.yml) installs Inno Setup via choco, runsbuild_windows_installer.ps1, and publishestelegram-download-chat-v<version>-setup.exeplusapp-<version>.zip(the in-app update asset) alongside the legacy one-filetelegram-download-chat.exe. The shipped iconassets/icon.icois a multi-size ICO (16–256 px); a 256-only ICO renders broken at small shortcut/taskbar sizes (guarded bytests/test_icon.py).
MCP Server
- Exposes
telegram_get_messagesandtelegram_connection_statustools - Uses task queue for serialized API calls
- Supports stdio (Claude Desktop) and HTTP transports
- Run with:
python -m telegram_download_chat.mcp
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository3 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- .cursor/rules/telegram-download-chat.mdcCursor rule
- telegram-download-chatSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

