Skip to content

Commit ebf5e3e

Browse files
committed
feat: add --deliver flag for odek run — delivers response to Telegram default chat
1 parent 9212d6f commit ebf5e3e

3 files changed

Lines changed: 114 additions & 3 deletions

File tree

cmd/odek/main.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"github.com/BackendStack21/kode"
1818
"github.com/BackendStack21/kode/internal/config"
19+
"github.com/BackendStack21/kode/internal/telegram"
1920
"github.com/BackendStack21/kode/internal/danger"
2021
"github.com/BackendStack21/kode/internal/llm"
2122
"github.com/BackendStack21/kode/internal/mcpclient"
@@ -316,6 +317,8 @@ type runFlags struct {
316317
// Repo context flags
317318
GithubRepoDirectory string // --github-repo-dir
318319
GithubRepoUrl string // --github-repo-url
320+
321+
Deliver *bool // nil = not set; true = deliver result to default channel
319322
}
320323

321324
// parseRunFlags parses `odek run` arguments and returns the parsed flags.
@@ -398,6 +401,9 @@ func parseRunFlags(args []string) (runFlags, error) {
398401
case "--ctx", "-c":
399402
f.Ctx = strings.Split(args[i+1], ",")
400403
i += 2
404+
case "--deliver":
405+
f.Deliver = boolPtr(true)
406+
i++
401407
default:
402408
// Not a flag — treat remaining as the task
403409
goto done
@@ -992,6 +998,7 @@ func run(args []string) error {
992998
// Shared agent run — capture messages for --learn mode
993999
var allMessages []llm.Message
9941000
var runErr error
1001+
var result string
9951002

9961003
if f.Session != nil && *f.Session {
9971004
// Multi-turn session mode: save conversation history
@@ -1007,9 +1014,7 @@ func run(args []string) error {
10071014
mm.AppendBuffer("user", shorten(f.Task, 100))
10081015
}
10091016

1010-
var result string
10111017
result, allMessages, runErr = agent.RunWithMessages(ctx, messages)
1012-
_ = result
10131018

10141019
// Append agent response to buffer
10151020
if runErr == nil && len(allMessages) > 0 {
@@ -1048,7 +1053,7 @@ func run(args []string) error {
10481053
if systemMessage != "" {
10491054
messages = append([]llm.Message{{Role: "system", Content: systemMessage}}, messages...)
10501055
}
1051-
_, allMessages, runErr = agent.RunWithMessages(ctx, messages)
1056+
result, allMessages, runErr = agent.RunWithMessages(ctx, messages)
10521057
}
10531058

10541059
if runErr != nil {
@@ -1075,6 +1080,32 @@ func run(args []string) error {
10751080
}
10761081
}
10771082

1083+
// ── Delivery: send result to default channel ──
1084+
if f.Deliver != nil && *f.Deliver && runErr == nil && result != "" {
1085+
if err := deliverToTelegram(result, resolved); err != nil {
1086+
fmt.Fprintf(os.Stderr, "odek: delivery failed: %v\n", err)
1087+
}
1088+
}
1089+
1090+
return nil
1091+
}
1092+
1093+
// deliverToTelegram sends a message to the configured Telegram default chat.
1094+
// Creates a temporary bot client from the resolved config and sends the
1095+
// response text. Returns an error if no Telegram config or chat is set.
1096+
func deliverToTelegram(text string, resolved config.ResolvedConfig) error {
1097+
if resolved.Telegram.Token == "" {
1098+
return fmt.Errorf("telegram bot_token not configured")
1099+
}
1100+
chatID := resolved.Telegram.DefaultChatID
1101+
if chatID == 0 {
1102+
return fmt.Errorf("telegram default_chat_id not configured")
1103+
}
1104+
bot := telegram.NewBot(resolved.Telegram.Token)
1105+
_, err := bot.SendMessage(chatID, text, &telegram.SendOpts{ParseMode: telegram.ParseModeMarkdownV2})
1106+
if err != nil {
1107+
return fmt.Errorf("send telegram message: %w", err)
1108+
}
10781109
return nil
10791110
}
10801111

cmd/odek/main_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/BackendStack21/kode/internal/danger"
1919
"github.com/BackendStack21/kode/internal/llm"
2020
"github.com/BackendStack21/kode/internal/mcpclient"
21+
"github.com/BackendStack21/kode/internal/telegram"
2122
)
2223

2324
func TestGetVersion_LdFlagsOverride(t *testing.T) {
@@ -2073,3 +2074,81 @@ func TestBuildSystemPrompt_FallsBackToDefault(t *testing.T) {
20732074
t.Error("repo URL should appear in prompt")
20742075
}
20752076
}
2077+
2078+
// ── --deliver flag tests ──────────────────────────────────────────────────
2079+
2080+
func TestParseRunFlags_Deliver(t *testing.T) {
2081+
f, err := parseRunFlags([]string{"--deliver", "test task"})
2082+
if err != nil {
2083+
t.Fatalf("parseRunFlags error: %v", err)
2084+
}
2085+
if f.Deliver == nil || !*f.Deliver {
2086+
t.Error("Deliver should be true when --deliver is passed")
2087+
}
2088+
if f.Task != "test task" {
2089+
t.Errorf("Task = %q, want %q", f.Task, "test task")
2090+
}
2091+
}
2092+
2093+
func TestParseRunFlags_DeliverDefaults(t *testing.T) {
2094+
f, err := parseRunFlags([]string{"test task"})
2095+
if err != nil {
2096+
t.Fatalf("parseRunFlags error: %v", err)
2097+
}
2098+
if f.Deliver != nil {
2099+
t.Error("Deliver should be nil when --deliver is not passed")
2100+
}
2101+
}
2102+
2103+
// TestDeliverToTelegram_MissingConfig tests error handling when config is missing.
2104+
func TestDeliverToTelegram_MissingConfig(t *testing.T) {
2105+
// No token
2106+
err := deliverToTelegram("hello", config.ResolvedConfig{})
2107+
if err == nil {
2108+
t.Error("expected error with empty token")
2109+
}
2110+
2111+
// Token but no default chat ID
2112+
err = deliverToTelegram("hello", config.ResolvedConfig{
2113+
Telegram: telegram.TelegramConfig{Token: "test:token"},
2114+
})
2115+
if err == nil {
2116+
t.Error("expected error with empty default_chat_id")
2117+
}
2118+
}
2119+
2120+
// TestDeliverToTelegram_SendsMessage tests that deliverToTelegram actually sends.
2121+
func TestDeliverToTelegram_SendsMessage(t *testing.T) {
2122+
// Mock Telegram API server
2123+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2124+
// Verify the request
2125+
if r.Method != http.MethodPost {
2126+
t.Errorf("expected POST, got %s", r.Method)
2127+
}
2128+
body, _ := io.ReadAll(r.Body)
2129+
if !strings.Contains(string(body), "test response") {
2130+
t.Errorf("expected message body to contain 'test response', got: %s", string(body))
2131+
}
2132+
if !strings.Contains(string(body), "8592463065") {
2133+
t.Errorf("expected chat_id 8592463065, got: %s", string(body))
2134+
}
2135+
// Return a valid Telegram API response
2136+
w.Header().Set("Content-Type", "application/json")
2137+
w.Write([]byte(`{"ok":true,"result":{"message_id":123}}`))
2138+
}))
2139+
defer srv.Close()
2140+
2141+
// Use mock server URL as Telegram API base by creating a bot with the mock URL
2142+
bot := telegram.NewBot("test:token")
2143+
bot.BaseURL = srv.URL // the mock server
2144+
2145+
// We can't patch deliverToTelegram's bot easily, so let's test via the config path
2146+
// Instead, test that the bot.SendMessage works correctly
2147+
msg, err := bot.SendMessage(8592463065, "test response", &telegram.SendOpts{ParseMode: telegram.ParseModeMarkdownV2})
2148+
if err != nil {
2149+
t.Fatalf("SendMessage error: %v", err)
2150+
}
2151+
if msg == nil || msg.ID != 123 {
2152+
t.Errorf("expected message_id 123, got %v", msg)
2153+
}
2154+
}

internal/telegram/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type TelegramConfig struct {
2222
HealthAddr string `json:"health_addr"` // e.g. "127.0.0.1:9090" (empty = disabled)
2323
LogLevel string `json:"log_level"` // "debug","info","warn","error" (default "info")
2424
LogFile string `json:"log_file"` // path or empty for stderr
25+
DefaultChatID int64 `json:"default_chat_id"` // for --deliver and cron delivery
2526
}
2627

2728
// DefaultConfig returns a TelegramConfig with sensible defaults.

0 commit comments

Comments
 (0)