@@ -5,84 +5,132 @@ import (
55 "fmt"
66 "os"
77 "os/signal"
8+ "strings"
89 "syscall"
910 "time"
1011
12+ "github.com/BackendStack21/kode"
13+ "github.com/BackendStack21/kode/internal/config"
14+ "github.com/BackendStack21/kode/internal/llm"
15+ "github.com/BackendStack21/kode/internal/render"
1116 "github.com/BackendStack21/kode/internal/session"
1217 "github.com/BackendStack21/kode/internal/telegram"
1318)
1419
1520// telegramCmd is the entry point for "odek telegram".
1621func telegramCmd (args []string ) error {
17- // 1. Load and validate config.
22+ // 1. Load config from all sources (file → env).
23+ resolved := config .LoadConfig (config.CLIFlags {})
24+
25+ // 2. Validate API key presence.
26+ if resolved .APIKey == "" {
27+ return fmt .Errorf ("no API key configured — set ODEK_API_KEY, DEEPSEEK_API_KEY, or configure in odek.json" )
28+ }
29+
30+ // 3. Load and validate Telegram config.
1831 cfg := telegram .ConfigFromEnv ()
1932 if err := telegram .ValidateConfig (cfg ); err != nil {
2033 fmt .Fprintf (os .Stderr , "odek telegram: %v\n " , err )
2134 return err
2235 }
2336
24- // 2 . Create bot client.
37+ // 4 . Create bot client.
2538 bot := telegram .NewBot (cfg .Token )
2639
27- // 3 . Create session store on disk (~/.odek/sessions/).
40+ // 5 . Create session store on disk (~/.odek/sessions/).
2841 store , err := session .NewStore ()
2942 if err != nil {
3043 fmt .Fprintf (os .Stderr , "odek telegram: session store: %v\n " , err )
3144 return err
3245 }
3346
34- // 4 . Create session manager (per-chat Telegram session cache).
47+ // 6 . Create session manager (per-chat Telegram session cache).
3548 sessionManager := telegram .NewSessionManager (store )
36- _ = sessionManager // will be used when the agent engine is wired in
3749
38- // 5 . Create handler.
50+ // 7 . Create handler.
3951 handler := telegram .NewHandler (bot )
4052
41- // 6 . Set handler config from cfg.
53+ // 8 . Set handler config from cfg.
4254 handler .Config = telegram.HandlerConfig {
4355 AllowedChats : cfg .AllowedChats ,
4456 BotUsername : cfg .BotUsername ,
4557 MaxMsgLength : cfg .MaxMsgLength ,
4658 AllowedUsers : cfg .AllowedUsers ,
4759 }
4860
49- // 7. Wire handler callbacks.
61+ // 9. Resolve system message.
62+ systemMessage := resolved .System
63+ if systemMessage == "" {
64+ systemMessage = defaultSystem
65+ }
66+
67+ // 10. Wire handler callbacks.
68+ //
69+ // Important: OnTextMessage processes in a background goroutine so it doesn't
70+ // block the main update processing loop. The TelegramApprover blocks waiting
71+ // for inline keyboard callbacks, which arrive via the main loop — only async
72+ // dispatch prevents deadlock.
5073 handler .OnTextMessage = func (chatID int64 , text string ) (string , error ) {
51- // Placeholder: load session, append user message, run agent,
52- // save session, return response.
53- // For now, just echo the text back as a placeholder.
54- return fmt .Sprintf ("Echo: %s" , text ), nil
74+ go handleChatMessage (chatID , text , bot , handler , sessionManager ,
75+ resolved , systemMessage )
76+ return "" , nil
5577 }
5678
5779 handler .OnCommand = func (chatID int64 , cmdName string , argsStr string ) (string , error ) {
5880 cmd := telegram .FindCommand (cmdName )
5981 if cmd == nil {
6082 return fmt .Sprintf ("Unknown command: /%s" , cmdName ), nil
6183 }
84+
85+ // Handle /new — clear session and reset trust in the approver.
86+ if cmdName == "new" {
87+ sessionManager .Delete (chatID )
88+ if handler .Approver != nil {
89+ handler .Approver .ResetTrust ()
90+ }
91+ }
92+
6293 return cmd .Handler (argsStr )
6394 }
6495
6596 handler .OnCallbackQuery = func (chatID int64 , data string ) (string , error ) {
66- return fmt .Sprintf ("Callback received: %s" , data ), nil
97+ return "" , nil // approval callbacks are routed by the approver
98+ }
99+
100+ handler .OnVoiceMessage = func (chatID int64 , fileID string ) (string , error ) {
101+ go handleChatMessage (chatID , "[voice message: " + fileID + "]" ,
102+ bot , handler , sessionManager , resolved , systemMessage )
103+ return "" , nil
104+ }
105+
106+ handler .OnPhotoMessage = func (chatID int64 , fileIDs []string ) (string , error ) {
107+ go handleChatMessage (chatID , "[photo message: " + strings .Join (fileIDs , "," )+ "]" ,
108+ bot , handler , sessionManager , resolved , systemMessage )
109+ return "" , nil
67110 }
68111
69112 handler .OnError = func (chatID int64 , err error ) {
70113 fmt .Fprintf (os .Stderr , "odek telegram: error for chat %d: %v\n " , chatID , err )
71114 }
72115
73- // 8. Print startup banner.
116+ // 11. Set command list via Telegram API.
117+ if err := bot .SetMyCommands (telegram .CommandDescriptors ()); err != nil {
118+ fmt .Fprintf (os .Stderr , "odek telegram: warning: set commands failed: %v\n " , err )
119+ }
120+
121+ // 12. Print startup banner.
74122 fmt .Fprintf (os .Stderr , "odek telegram bot started\n " )
75123
76- // 9 . Create poller.
124+ // 13 . Create poller.
77125 poller := telegram .NewPoller (bot )
78126 poller .Interval = time .Duration (cfg .PollInterval ) * time .Second
79127 poller .Timeout = cfg .PollTimeout
80128
81- // 10 . Create cancellable context for graceful shutdown.
129+ // 14 . Create cancellable context for graceful shutdown.
82130 ctx , cancel := context .WithCancel (context .Background ())
83131 defer cancel ()
84132
85- // 11 . Handle SIGINT/SIGTERM for graceful shutdown.
133+ // 15 . Handle SIGINT/SIGTERM for graceful shutdown.
86134 sigCh := make (chan os.Signal , 1 )
87135 signal .Notify (sigCh , syscall .SIGINT , syscall .SIGTERM )
88136 go func () {
@@ -91,14 +139,97 @@ func telegramCmd(args []string) error {
91139 cancel ()
92140 }()
93141
94- // 12 . Start polling in a background goroutine.
142+ // 16 . Start polling in a background goroutine.
95143 updates := make (chan telegram.Update , 100 )
96144 go poller .Start (ctx , updates )
97145
98- // 13 . Process updates until the channel is closed (ctx cancelled).
146+ // 17 . Process updates until the channel is closed (ctx cancelled).
99147 for upd := range updates {
100148 handler .HandleUpdate (upd )
101149 }
102150
103151 return nil
104152}
153+
154+ // handleChatMessage processes a user message from Telegram in a background
155+ // goroutine. It creates or loads the chat session, creates a TelegramApprover
156+ // for approval prompts, runs the agent loop with RunWithMessages, and sends
157+ // back the response. Each chat gets its own TelegramApprover instance.
158+ func handleChatMessage (
159+ chatID int64 ,
160+ text string ,
161+ bot * telegram.Bot ,
162+ handler * telegram.Handler ,
163+ sessionManager * telegram.SessionManager ,
164+ resolved config.ResolvedConfig ,
165+ systemMessage string ,
166+ ) {
167+ // Create a per-chat TelegramApprover for inline keyboard approval.
168+ approver := telegram .NewTelegramApprover (bot , chatID )
169+ handler .Approver = approver
170+
171+ // Get or create the session for this chat.
172+ cs , err := sessionManager .GetOrCreate (chatID )
173+ if err != nil {
174+ reportError (bot , chatID , "Failed to create session: " + err .Error ())
175+ return
176+ }
177+
178+ // Append user message to session.
179+ cs .Messages = append (cs .Messages , llm.Message {Role : "user" , Content : text })
180+ cs .LastActive = time .Now ()
181+
182+ // Build the agent with Telegram approver.
183+ tools := builtinTools (resolved .Dangerous , nil , approver , resolved .MaxConcurrency )
184+
185+ modelLabel := odek .ProfileLabel (resolved .Model )
186+ if modelLabel == "" {
187+ modelLabel = "deepseek-chat"
188+ }
189+
190+ rend := render .New (os .Stderr , false ).WithModel (modelLabel )
191+
192+ agent , err := odek .New (odek.Config {
193+ Model : resolved .Model ,
194+ BaseURL : resolved .BaseURL ,
195+ APIKey : resolved .APIKey ,
196+ MaxIterations : resolved .MaxIter ,
197+ SystemMessage : systemMessage ,
198+ NoProjectFile : resolved .NoAgents ,
199+ Thinking : resolved .Thinking ,
200+ Tools : tools ,
201+ Renderer : rend ,
202+ })
203+ if err != nil {
204+ reportError (bot , chatID , "Failed to create agent: " + err .Error ())
205+ return
206+ }
207+ defer agent .Close ()
208+
209+ // Run the agent with the full message history (multi-turn).
210+ response , updatedMessages , err := agent .RunWithMessages (context .Background (), cs .Messages )
211+ if err != nil {
212+ reportError (bot , chatID , "Agent error: " + err .Error ())
213+ return
214+ }
215+
216+ // Save the updated session messages.
217+ cs .Messages = updatedMessages
218+ cs .TurnCount ++
219+ if err := sessionManager .Save (chatID , cs .Messages ); err != nil {
220+ fmt .Fprintf (os .Stderr , "odek telegram: session save: %v\n " , err )
221+ }
222+
223+ // Send the response.
224+ if response != "" {
225+ handler .SendResponse (chatID , response )
226+ }
227+ }
228+
229+ // reportError sends an error message to the given chat and logs to stderr.
230+ func reportError (bot * telegram.Bot , chatID int64 , msg string ) {
231+ fmt .Fprintf (os .Stderr , "odek telegram: %s\n " , msg )
232+ if _ , err := bot .SendMessage (chatID , "❌ " + msg , nil ); err != nil {
233+ fmt .Fprintf (os .Stderr , "odek telegram: send error message: %v\n " , err )
234+ }
235+ }
0 commit comments