An agent-first Telegram CLI, written in Go.
Speaks MTProto through gotgproto and gotd/td — writes compact JSONL to stdout and structured JSON errors to stderr, so an agent can pipe and parse every result.
Русская версия · Install · Quick start · Commands · Docs
$ tgc send @user "**hi**" --await-reply
{"id":42,"chat_id":123,"date":"2026-07-19T12:00:00Z","text":"hi","sender_id":123}
{"id":43,"chat_id":123,"date":"2026-07-19T12:00:04Z","text":"hey!","sender_id":456}Pass --pretty when a human is reading.
Recommended (no Go required — downloads a release binary):
curl -fsSL https://github.com/raw/grigoreo-dev/tgc/main/install.sh | shEnv knobs: TGC_VERSION=vX.Y.Z for a specific version, TGC_INSTALL_DIR for a
custom directory (default ~/.local/bin), GITHUB_TOKEN/GH_TOKEN to avoid API
rate limits, TGC_NO_SETUP=1 to skip post-install PATH/completion setup.
After install, the script runs tgc self setup so new shell sessions find
tgc on PATH and get bash/zsh/fish completion (per-user). Setup failure is a
warning only — the binary install still succeeds. Opt out with
TGC_NO_SETUP=1. Re-run anytime with tgc self setup; reverse with
tgc self setup --remove (removes only the managed rc block and marked files).
tgc completion <shell> generates a completion script to stdout (for manual
install). tgc self setup installs the same scripts into standard
per-user locations. Prefer setup for day-to-day use.
zsh: the managed ~/.zshrc block updates fpath before completion loads —
keep compinit after that block (default layouts usually already do).
Prefer to inspect first? Download install.sh, read it, then run it.
Alternative (requires Go 1.25+):
go install github.com/grigoreo-dev/tgc/cmd/tgc@latestA
go installbuild reports versiondevand does not auto-check for updates or self-update. For automatic update checks andtgc self update, install via the script above. Aftergo install, runtgc self setuponce if you want PATH/completion wired the same way.
tgc self update # download and install the latest release
tgc self check # report {"update_available":...} without installingWhile a newer release is available, tgc prints a one-line
{"warning":"update_available",...} to stderr on each run. Set
TGC_NO_UPDATE_CHECK=1 to disable the check entirely. After a successful
update, marked completion files installed by tgc self setup are refreshed.
Security: releases are verified by sha256 checksum over HTTPS. Publisher signature verification (cosign) is planned for a future release.
Get an api_id and api_hash at my.telegram.org,
then log in:
tgc auth login --phone +NNN # user account
tgc auth login --bot-token $TOKEN # bot accountOnce logged in:
tgc chats
tgc send @user "**hi**"
tgc read @user --limit 10Run tgc --help for the full list; tgc <command> --help for each command's
flags.
| Command | Purpose |
|---|---|
auth |
Manage Telegram sessions (login, list, export, import, logout). |
chats |
List dialogs (cached 5m; --fresh to refresh). |
info |
Show a chat or user card (id, type, title; username, phone, about, members_count, bot, premium when set). |
members |
List the members of a group. |
search |
Search chats and messages (both by default); --type chats|messages|user|group|channel to narrow; --chat <peer> to search inside one chat (--from, --since, --until). |
read |
Read chat history, newest first. |
await |
Block until incoming messages arrive, print them, mark them read. |
context |
Show a message with the messages surrounding it. |
send |
Send a message (Markdown by default; --file for media; --await-reply to wait for the reply). |
edit |
Edit a message you sent. |
delete |
Delete messages (for everyone by default; --for-me to keep for others). |
forward |
Forward a message to another chat. |
download |
Download media from a message. |
- stdout carries results only, as compact JSONL — one JSON object per line. This is the default so agents can pipe output straight into a parser.
- stderr carries errors, as a single structured JSON line
(
{"error":"<code>","message":"..."}), and the process exits 1. --prettyswitches stdout to human-readable tables instead of JSONL.
Errors use a stable set of machine-readable codes:
| Code | Meaning |
|---|---|
ambiguous |
A selector matched more than one chat; candidates are in the error body. |
bad_args |
Invalid arguments or flags. |
bad_config |
The config file or an env var could not be parsed. |
bot_unsupported |
The command is not available for bot accounts. |
flood_wait |
Telegram rate-limited the request; retry after the wait. |
io_error |
A local filesystem operation failed. |
no_api_credentials |
api_id/api_hash are not set. |
no_media |
The target message has no downloadable media. |
not_authenticated |
No session for the selected profile. |
not_found |
The chat, user, or message could not be resolved. |
rich_unsupported |
The account rejected a --rich message. |
upload_failed |
An uploaded file did not come back usable. |
| Variable | Effect |
|---|---|
TGC_PROFILE |
The active profile (same as --profile). |
TGC_API_ID |
Telegram api_id. |
TGC_API_HASH |
Telegram api_hash. |
TGC_SESSION |
Import a session string directly, without a session file. |
TGC_CONFIG_DIR |
Override the config/profile root. XDG_CONFIG_HOME is honored too. |
TGC_DOWNLOAD_DIR |
Download root; defaults to ~/.tgc/downloads. |
NO_COLOR |
Disable --pretty colors. |
tgc keeps named profiles, selected with --profile or TGC_PROFILE. Each
profile stores its session under the config directory, and a profile is either a
user login or a bot login. This lets you keep, say, a personal account and a bot
side by side and switch between them per command.
Run tgc init in a project directory to create a local ./.tgc config, so that
directory (and its subdirectories) uses its own default account:
tgc init --profile work
tgc auth login
tgc discovers config in this order: TGC_CONFIG_DIR → the nearest ./.tgc
walking up from the current directory (not past your home directory) →
$XDG_CONFIG_HOME/tgc → ~/.config/tgc. A shared parent workspace/.tgc
covers all subprojects; a nearer ./.tgc overrides it. A .tgc directly in
your home directory is not used as local config (that is what ~/.config/tgc
is for).
tgc init writes .tgc/.gitignore (*) so sessions are never committed, and
inherits api_id/api_hash from your global config if set. Inspect the active
config with:
tgc config path
A bot account can't list dialogs or run search — both return
bot_unsupported, because Telegram doesn't expose dialog listing or contacts
search to bots. A bot can send and read in chats it belongs to: address a user
by @username, or by numeric user id for someone who has already messaged the
bot.
When an agent needs the other side's reply rather than a snapshot of history,
tgc await blocks until unread messages arrive, prints them, marks them read,
and exits — the wait-for-reply primitive, no polling loop required.
tgc await @user # block until a message arrives
tgc await @user --timeout 30 # give up after 30s
tgc send @bot "ping" --await-reply # send, then wait for the reply on one connectionawait debounces: it waits for a quiet gap before returning, so a burst (or a
media group) is coalesced into a single batch. Messages print oldest→newest, one
compact JSON object per line — the same shape as read:
{"id":42,"chat_id":123,"date":"2026-07-19T12:00:00Z","text":"hi","sender_id":123, ...}Telegram rich messages (Bot API 10.1) carry their body in a rich block tree
rather than plain text. read/context/await render that body to Markdown in
the text field and add "rich": true; if the copy was truncated (or a full
fetch was skipped/failed), "rich_truncated": true is also set.
On silence until the timeout, await prints a marker and exits 0 (a normal
outcome, not an error):
{"status":"timeout","chat_id":123,"waited":30}| Flag | Command | Default | Effect |
|---|---|---|---|
--timeout |
await |
300 |
Max seconds to wait before emitting the timeout marker. |
--debounce |
await |
2 |
Seconds of quiet before returning a batch (0 = off). |
--from |
await |
— | Only messages from this sender (selector). |
--await-reply |
send |
false |
After sending, wait for the reply on the same connection. |
--await-timeout |
send |
300 |
--await-reply: max seconds to wait. |
--await-debounce |
send |
2 |
--await-reply: quiet seconds before returning a batch. |
--await-from |
send |
— | --await-reply: only messages from this sender. |
Read-receipt side effect:
awaitmarks the awaited messages read, which is a visible action to the contact (they see your "read" state advance). This is intentional — the messages are consumed — but it is not a silent peek.
One await per profile: a profile holds a single MTProto session, so you cannot run two
awaits (or asend --await-replyalongside anotherawait) concurrently on the same profile. Parallel agents must use separate profiles (--profile/TGC_PROFILE, or a per-project./.tgc).
Bots are live-only: no unread backfill and no mark-read, so a bot
awaitcatches only messages that arrive while it is waiting.--await-replyis not supported together with--file.
A dev-only bot-side harness for exercising these paths lives at scripts/await-e2e.sh.
Sending. By default, Markdown renders through Telegram message entities,
which every account supports. A server-side rich-message path also exists, but
user accounts reject it with RICH_MESSAGE_UNSUPPORTED, so tgc falls back to
entities transparently. See docs/rich-spike.md for the
full investigation and the live result.
Reading. Telegram Rich Messages (Bot API 10.1) put their body in a rich block
tree (tg.Message.RichMessage) with an empty plain text field. read,
context, and await render that block tree — headings, bold/italic/spoiler,
code, lists, blockquotes, tables, math, and inline media references — into
Markdown in the text field and add "rich": true. If the inline copy was
truncated (or a full fetch was skipped/failed), "rich_truncated": true is also
set. Untrusted text is Markdown-escaped so a sender cannot forge formatting.
Contributions are welcome. The workflow mirrors CI, so what passes locally passes in the pipeline.
Setup
git clone https://github.com/grigoreo-dev/tgc
cd tgc
go build ./...Before you open a PR, run the same checks CI does:
go build ./...
go vet ./...
go test ./...
shellcheck install.sh # if you touched the install scriptKeep changes focused, follow the existing package layout under internal/, and
preserve the output contract — results as JSONL on stdout,
structured errors on stderr. User-facing changes should update the README (and
README.ru.md).
Live e2e suite. scripts/e2e/ holds a bidirectional live test suite that
drives tgc against real test accounts (a user and a bot). It is not run by CI —
it needs credentials and dedicated throwaway accounts. See
scripts/e2e/README.md for preconditions and how to run
scripts/e2e/run-all.sh.
Release versions and CHANGELOG.md are generated from
Conventional Commits. Use squash merge
and give ordinary PRs a title such as fix: handle empty chat,
feat: add search, or feat(cli)!: change the output contract. CI rejects
other titles (conventional-commit-title). Release Please opens a Release PR
whose generated title (for example chore(main): release 0.2.0) is
Conventional-Commit-compatible, so that check must pass on the Release PR as
well.
After releasable commits reach main, Release Please
opens or updates that Release PR with the next version and changelog. Review
it, then merge when the release is ready — do not create release tags by hand.
On merge of the Release PR, the release workflow:
- Runs build, vet, test, and
install.shshellcheck (verify) on the merge commit onmain. - Lets Release Please create the
vX.Y.Zgit tag and a draft GitHub Release (notes/body from the generated changelog). - Runs GoReleaser against that tag to attach archives and
checksums.txtto the existing draft (retry-safe replace). - Publishes the release with
gh release edit … --draft=falseonly after assets succeed.
To rebuild assets for an existing draft that is not yet published, without
re-running Release Please, use Actions → Release → Run workflow and pass
the existing tag_name (for example v0.2.0). That path validates the tag,
checks out that tag for verify and GoReleaser, then republishes the same
draft after assets succeed. Do not use it for already-published releases.
While tgc is below v1.0.0, breaking changes release the next 0.x.0 version
(bump-minor-pre-major). v1.0.0 is an explicit maintainer decision, not
an automatic consequence of a breaking-change commit.
Repository settings (maintainer / admin, not enforced by workflow YAML):
- Squash merge — Settings → General → Pull Requests: enable squash merging; prefer defaulting the squash commit message to the PR title so validated titles become the commits Release Please reads.
- Required checks on
main— branch protection or ruleset: require status checks before merge; include at leasttest,lint, andconventional-commit-title; prefer requiring branches to be up to date. With the built-inGITHUB_TOKEN, Release Please PRs are opened bygithub-actions[bot], soconventional-commit-titleruns and must pass (the generated title is Conventional-Commit-compatible). Treat Release PRs like any other PR for required checks; do not rely on a Skipped title job.
Local release-config contract (no credentials, no publish):
sh scripts/check-release-config.sh
docker run --rm -v "$PWD":/work -w /work goreleaser/goreleaser:v2.12.7 check --config .goreleaser.yamlUse a concrete GoReleaser image tag such as v2.12.7 for check; the floating
tag goreleaser/goreleaser:v2 is not a published image.
This repo tracks work with beads (bd),
a git-native, dependency-aware issue tracker. You don't need it to send a PR, but
if you want to pick up existing work or coordinate a larger change:
bd ready # list unblocked, claimable work
bd show <id> # read an issue and its dependencies
bd update <id> --claim # atomically take it (sets you as assignee)
bd close <id> --reason "..." # mark it doneAs an outside contributor, initialise beads in contributor mode so your planning issues route to a separate local repo and stay out of your PR:
bd init --contributorReference the bead id in your commit messages (e.g. tgc-a1b2: fix flood-wait backoff) so the change is traceable back to its issue.
- Design spec: docs/superpowers/specs/2026-07-13-tgc-telegram-cli-design.md
- Implementation plan: docs/superpowers/plans/2026-07-14-tgc-v1-implementation.md
- Live integration checklist: docs/integration-checklist.md
- RichMessage spike: docs/rich-spike.md