[{"data":1,"prerenderedAt":350},["ShallowReactive",2],{"content-tgdyk":3},{"id":4,"title":5,"articleTitleSource":6,"articleValid":7,"articleWarnings":8,"body":9,"comments":7,"date":335,"date_updated":8,"description":336,"extension":337,"icon":338,"image":339,"language":340,"meta":341,"navigation":7,"path":342,"publish-to":343,"readingTime":344,"seo":346,"stem":33,"titleEasterEgg":8,"titleLines":347,"topics":348,"translationKey":33,"__hash__":349},"content/tgdyk.md","tgdyk: Telegram as an event stream for local agents","h1",true,null,{"type":10,"value":11,"toc":328},"minimark",[12,18,22,25,35,40,43,86,89,121,124,151,154,158,161,164,258,261,264,268,275,285,291,301,305,310,314,317,324],[13,14],"external-link-card",{"icon":15,"title":16,"url":17},"streamline-ultimate-color:github-logo-1","pavel-voronin/tgdyk","https://github.com/pavel-voronin/tgdyk",[19,20,21],"p",{},"Telegram is where signals live for many of us: work chats, channels, alerts, direct messages. If you're building an agent or any automation, sooner or later you face the same task: giving your tooling eyes inside your Telegram.",[19,23,24],{},"The Bot API doesn't work for this. A bot is a separate entity: it doesn't see your dialogs, doesn't read the channels you follow, and only operates where it has been explicitly added. You need a user session. Libraries like Telethon or gramjs solve the problem, but every script drags a full client along with it: authorization, session storage, reconnects. Once you have several tools, this turns into duplicating the same infrastructure over and over.",[19,26,27,28,34],{},"I wrote ",[29,30,33],"a",{"href":17,"rel":31},[32],"nofollow","tgdyk"," — a tiny local daemon that solves exactly one problem: it holds a single Telegram user session and hands out live TDLib updates to local tools as NDJSON.",[36,37,39],"h2",{"id":38},"how-it-works","How it works",[19,41,42],{},"A single Rust binary on top of TDLib. Four commands:",[44,45,46,68,74,80],"ul",{},[47,48,49,53,54,57,58,61,62,67],"li",{},[50,51,52],"code",{},"setup"," — enter your ",[50,55,56],{},"api_id"," and ",[50,59,60],{},"api_hash"," from ",[29,63,66],{"href":64,"rel":65},"https://my.telegram.org/apps",[32],"my.telegram.org/apps",", your phone number and the code. Once.",[47,69,70,73],{},[50,71,72],{},"daemon"," — starts TDLib and publishes updates over a local Unix socket.",[47,75,76,79],{},[50,77,78],{},"stream"," — prints raw TDLib updates line by line as JSON.",[47,81,82,85],{},[50,83,84],{},"doctor"," — checks TDLib, the config, the paths, and the connection to the daemon.",[19,87,88],{},"From there it's plain Unix composition:",[90,91,96],"pre",{"className":92,"code":93,"language":94,"meta":95,"style":95},"language-bash shiki shiki-themes material-theme","./tgdyk stream | jq .\n","bash","",[50,97,98],{"__ignoreMap":95},[99,100,103,107,111,115,118],"span",{"class":101,"line":102},"line",1,[99,104,106],{"class":105},"s5Dmg","./tgdyk",[99,108,110],{"class":109},"sfyAc"," stream",[99,112,114],{"class":113},"sAklC"," |",[99,116,117],{"class":105}," jq",[99,119,120],{"class":109}," .\n",[19,122,123],{},"New text messages only:",[90,125,127],{"className":92,"code":126,"language":94,"meta":95,"style":95},"./tgdyk stream | jq -r 'select(.[\"@type\"] == \"updateNewMessage\") | .message.content.text.text // empty'\n",[50,128,129],{"__ignoreMap":95},[99,130,131,133,135,137,139,142,145,148],{"class":101,"line":102},[99,132,106],{"class":105},[99,134,110],{"class":109},[99,136,114],{"class":113},[99,138,117],{"class":105},[99,140,141],{"class":109}," -r",[99,143,144],{"class":113}," '",[99,146,147],{"class":109},"select(.[\"@type\"] == \"updateNewMessage\") | .message.content.text.text // empty",[99,149,150],{"class":113},"'\n",[19,152,153],{},"That's where tgdyk's responsibility ends. Everything downstream — filtering, routing, reacting — is assembled from what you already have: shell, jq, a Python script, an LLM call.",[36,155,157],{"id":156},"why-agents-need-this","Why agents need this",[19,159,160],{},"The typical scenario: an agent runs in the background, and you want Telegram events to reach it without polling and without you in the loop. A message in a work chat, an alert from a monitoring channel, a reply to your post — all of it arrives in the stream as structured JSON, and what happens next is up to you: wake the agent, push to a queue, hit a webhook.",[19,162,163],{},"A pipeline is one line:",[90,165,167],{"className":92,"code":166,"language":94,"meta":95,"style":95},"./tgdyk stream \\\n  | jq -c 'select(.[\"@type\"] == \"updateNewMessage\")' \\\n  | while read -r msg; do\n      # your handler goes here: a script, a queue, an agent call\n      echo \"$msg\" >> inbox.ndjson\n    done\n",[50,168,169,179,200,224,231,252],{"__ignoreMap":95},[99,170,171,173,175],{"class":101,"line":102},[99,172,106],{"class":105},[99,174,110],{"class":109},[99,176,178],{"class":177},"svy0-"," \\\n",[99,180,182,185,187,190,192,195,198],{"class":101,"line":181},2,[99,183,184],{"class":113},"  |",[99,186,117],{"class":105},[99,188,189],{"class":109}," -c",[99,191,144],{"class":113},[99,193,194],{"class":109},"select(.[\"@type\"] == \"updateNewMessage\")",[99,196,197],{"class":113},"'",[99,199,178],{"class":177},[99,201,203,205,209,213,215,218,221],{"class":101,"line":202},3,[99,204,184],{"class":113},[99,206,208],{"class":207},"s6cf3"," while",[99,210,212],{"class":211},"sdLwU"," read",[99,214,141],{"class":109},[99,216,217],{"class":109}," msg",[99,219,220],{"class":113},";",[99,222,223],{"class":207}," do\n",[99,225,227],{"class":101,"line":226},4,[99,228,230],{"class":229},"s0_hs","      # your handler goes here: a script, a queue, an agent call\n",[99,232,234,237,240,243,246,249],{"class":101,"line":233},5,[99,235,236],{"class":211},"      echo",[99,238,239],{"class":113}," \"",[99,241,242],{"class":177},"$msg",[99,244,245],{"class":113},"\"",[99,247,248],{"class":113}," >>",[99,250,251],{"class":109}," inbox.ndjson\n",[99,253,255],{"class":101,"line":254},6,[99,256,257],{"class":207},"    done\n",[19,259,260],{},"No SDK, no language lock-in. If a tool can read stdin or a Unix socket, it can read your Telegram.",[19,262,263],{},"You don't even have to write these pipelines by hand. Modern agents are fluent in the CLI and Unix composition — jq, streams, redirects, systemd units. Tell your agent \"subscribe to channel X and put the alerts in a queue,\" and it will build the plumbing itself. tgdyk gives it a primitive that this assembles from reliably.",[36,265,267],{"id":266},"design-decisions","Design decisions",[19,269,270,274],{},[271,272,273],"strong",{},"Raw updates."," tgdyk doesn't invent its own schema on top of TDLib. The TDLib types are the contract: they're documented, stable, and cover everything that happens in an account. Any wrapper would mean losing information and adding one more layer to maintain.",[19,276,277,280,281,284],{},[271,278,279],{},"Live only."," tgdyk doesn't store history and has no replay. If you need persistence, it's one line in your pipeline (",[50,282,283],{},">> inbox.ndjson","), and you choose the format and the storage yourself. The only thing on disk is TDLib's standard local database with the session and its cache.",[19,286,287,290],{},[271,288,289],{},"A local Unix socket."," No ports, no tokens, no auth layer. Access to the stream is governed by filesystem permissions. This is a deliberate narrowing: tgdyk is a tool for your machine, not a network service.",[19,292,293,296,297,300],{},[271,294,295],{},"TDLib included."," Building TDLib is a well-known pain. Releases bundle ",[50,298,299],{},"libtdjson"," for macOS (Intel and Apple Silicon) and Linux (x64 and arm64), so the path from download to stream is a few commands.",[36,302,304],{"id":303},"limitations","Limitations",[19,306,307,309],{},[50,308,78],{}," only delivers what happens after you connect — start it before the events you want to catch. The daemon runs in the foreground: systemd, tmux, or a supervisor, your choice. And most importantly: this is your user session. Anything that connects to the socket sees your entire account — treat the machine running the daemon accordingly.",[36,311,313],{"id":312},"summary","Summary",[19,315,316],{},"tgdyk occupies one narrow spot in the pipeline: a live bridge between your Telegram account and local tools. Everything else is your composition.",[19,318,319,320],{},"Rust, MIT, binary releases for macOS and Linux: ",[29,321,323],{"href":17,"rel":322},[32],"github.com/pavel-voronin/tgdyk",[325,326,327],"style",{},"html pre.shiki code .s5Dmg, html code.shiki .s5Dmg{--shiki-default:#FFCB6B}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svy0-, html code.shiki .svy0-{--shiki-default:#EEFFFF}html pre.shiki code .s6cf3, html code.shiki .s6cf3{--shiki-default:#89DDFF;--shiki-default-font-style:italic}html pre.shiki code .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .s0_hs, html code.shiki .s0_hs{--shiki-default:#546E7A;--shiki-default-font-style:italic}",{"title":95,"searchDepth":226,"depth":226,"links":329},[330,331,332,333,334],{"id":38,"depth":181,"text":39},{"id":156,"depth":181,"text":157},{"id":266,"depth":181,"text":267},{"id":303,"depth":181,"text":304},{"id":312,"depth":181,"text":313},"2026-07-07","A tiny local Rust daemon over TDLib that holds one Telegram user session and exposes live raw updates as NDJSON for agents and pipelines.","md","streamline-ultimate-color:send-email-fly","og-image.jpg","en",{},"/tgdyk","all",{"wordCount":345},815,{"title":5,"description":336},"2","AI toolchain, Telegram","UP9NciIW-c5lSA-X4qDCJvWV38mxlJneLicrLbQmq0g",1783484430451]