Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
824c843
feat: beautify CLI logging with 256-color palette and improve connect…
ChenZhu-Xie Mar 10, 2026
0fd9ccf
chore: save current bot state before auditing and refactoring
ChenZhu-Xie Mar 11, 2026
537dba5
feat: implement robust context monitoring and session management for …
ChenZhu-Xie Mar 11, 2026
36fa1b6
feat: add session stats (workdir, id, usage) footer to Gemini responses
ChenZhu-Xie Mar 11, 2026
114e363
feat: add Chinese help command and localization support
ChenZhu-Xie Mar 11, 2026
b5c0467
refactor: rename sreset to ssnew and implement non-destructive sessio…
ChenZhu-Xie Mar 11, 2026
876c9e8
feat: enable telegram markdown with plain text fallback
ChenZhu-Xie Mar 11, 2026
6f269b6
docs: sync help text with ssnew command rename
ChenZhu-Xie Mar 11, 2026
e0fbf6f
feat: upgrade to HTML parse mode, absolute paths, and descriptive ses…
ChenZhu-Xie Mar 11, 2026
6cfbff4
fix: resolve Gemini session history listing and improve Telegram Mark…
ChenZhu-Xie Mar 11, 2026
be2c03f
opt: enhance session management and ssls output
ChenZhu-Xie Mar 11, 2026
0218e98
fix: add missing sort import in acp.go
ChenZhu-Xie Mar 11, 2026
bbc7d35
feat: improve session handling, bot stability, and formatting
ChenZhu-Xie Mar 12, 2026
ef0fcbd
feat: implement AST-based Telegram formatting and native Gemini sessi…
ChenZhu-Xie Mar 12, 2026
ed5072c
fix: restore machine-readable Gemini session listing via history files
ChenZhu-Xie Mar 12, 2026
c357440
fix: implement ssls/sssw for ACP sessions via shared file-scan helper
ChenZhu-Xie Mar 12, 2026
79db1ad
fix: parse Gemini session JSON correctly (user content is [{text}] ar…
ChenZhu-Xie Mar 12, 2026
3ae53e1
fix: rune-safe UTF-8 truncation for session summaries sent to Telegram
ChenZhu-Xie Mar 12, 2026
c7bea6a
feat: standardise Gemini session ID format across ssls and status bar
ChenZhu-Xie Mar 12, 2026
c4b0b4a
not fixied: the "pipe is being closed" and "Internal error" issue!
ChenZhu-Xie Mar 12, 2026
7e452e1
fix(acp): mark session inactive on NewSession failure to prevent inva…
ChenZhu-Xie Mar 12, 2026
b4531eb
feat: resolve issue 5 follow-up feedback and improve session ID forma…
ChenZhu-Xie Mar 12, 2026
81454a0
feat: improve markdown table alignment and latex rendering
ChenZhu-Xie Mar 13, 2026
64efee3
feat: refine latex rendering for fractions, limits and operators
ChenZhu-Xie Mar 13, 2026
231c480
chore: sync before link formatting fix
ChenZhu-Xie Mar 13, 2026
92313a9
feat: refine link formatting in Gemini and ACP adapters
ChenZhu-Xie Mar 13, 2026
6849876
feat: make clibot session names clickable in slist and status
ChenZhu-Xie Mar 13, 2026
dfcea77
chore: sync before starting tasks
ChenZhu-Xie Mar 13, 2026
ee81c3c
feat: update task list emoji and refine footnote rendering
ChenZhu-Xie Mar 13, 2026
1fd9952
feat: implement snewg command and initial help UI enhancements
ChenZhu-Xie Mar 13, 2026
5e19f19
chore: sync before rendering update
ChenZhu-Xie Mar 13, 2026
5580867
feat: render \cdot as · and fix LaTeX fraction bracket nesting
ChenZhu-Xie Mar 13, 2026
987d531
fix: improve help command rendering and update tests
ChenZhu-Xie Mar 13, 2026
3280a19
feat: robust clickable help links with smart pre-fill and default HTM…
ChenZhu-Xie Mar 13, 2026
b01108c
feat: unified and enhanced Help UI with categorized sections and comp…
ChenZhu-Xie Mar 13, 2026
2be5af6
fix: make ssnew instant by clearing session ID in ACPAdapter
ChenZhu-Xie Mar 13, 2026
4d69648
feat: refactor footnote rendering style to [sup]
ChenZhu-Xie Mar 14, 2026
f9eadeb
feat: enhance telegram html rendering with underline, spoilers, and e…
ChenZhu-Xie Mar 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added bot_test_output.txt
Binary file not shown.
39 changes: 21 additions & 18 deletions cmd/clibot/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,39 +79,39 @@ var (
// Start engine in a goroutine
engineErrChan := make(chan error, 1)
go func() {
fmt.Println("clibot engine starting...")
fmt.Println("Press Ctrl+C to stop")
logger.Info("clibot engine starting...")
logger.Info("Press Ctrl+C to stop")
engineErrChan <- engine.Run(ctx)
}()

// Wait for signal or engine error
select {
case sig := <-sigChan:
log.Printf("\nReceived signal: %v, shutting down gracefully...", sig)
logger.Warnf("Received signal: %v, shutting down gracefully...", sig)
cancel() // Cancel context to stop event loop
if err := engine.Stop(); err != nil {
log.Printf("Error during shutdown: %v", err)
logger.Errorf("Error during shutdown: %v", err)
}
case err := <-engineErrChan:
if err != nil {
log.Fatalf("Engine error: %v", err)
logger.Fatalf("Engine error: %v", err)
}
}

// Wait for engine to actually stop (with timeout via second Ctrl+C)
select {
case sig := <-sigChan:
log.Printf("\nReceived second signal: %v, forcing shutdown...", sig)
logger.Warnf("Received second signal: %v, forcing shutdown...", sig)
if err := engine.Stop(); err != nil {
log.Printf("Error during forced shutdown: %v", err)
logger.Errorf("Error during forced shutdown: %v", err)
}
case err := <-engineErrChan:
if err != nil {
log.Fatalf("Engine error: %v", err)
logger.Fatalf("Engine error: %v", err)
}
}

log.Println("Clibot stopped")
logger.Info("clibot stopped")
},
}
)
Expand Down Expand Up @@ -188,7 +188,7 @@ func registerCLIAdapters(engine *core.Engine, config *core.Config) error {
Env: cliConfig.Env,
})
default:
log.Printf("Warning: CLI adapter type '%s' not implemented yet", cliType)
logger.Warnf("Warning: CLI adapter type '%s' not implemented yet", cliType)
continue
}

Expand All @@ -197,7 +197,7 @@ func registerCLIAdapters(engine *core.Engine, config *core.Config) error {
}

engine.RegisterCLIAdapter(cliType, adapter)
log.Printf("Registered %s CLI adapter (mode: hook)", cliType)
logger.Infof("Registered %s CLI adapter (mode: hook)", cliType)
}

return nil
Expand All @@ -207,7 +207,7 @@ func registerCLIAdapters(engine *core.Engine, config *core.Config) error {
func registerBotAdapters(engine *core.Engine, config *core.Config) error {
for botType, botConfig := range config.Bots {
if !botConfig.Enabled {
log.Printf("Bot %s is disabled, skipping", botType)
logger.Infof("Bot %s is disabled, skipping", botType)
continue
}

Expand All @@ -218,7 +218,7 @@ func registerBotAdapters(engine *core.Engine, config *core.Config) error {
discordBot := bot.NewDiscordBot(botConfig.Token, botConfig.ChannelID)
discordBot.SetProxyManager(engine.GetProxyManager())
botAdapter = discordBot
log.Printf("Registered %s bot adapter", botType)
logger.Infof("Registered %s bot adapter", botType)

case "feishu":
feishuBot := bot.NewFeishuBot(botConfig.AppID, botConfig.AppSecret)
Expand All @@ -230,28 +230,31 @@ func registerBotAdapters(engine *core.Engine, config *core.Config) error {
}
feishuBot.SetProxyManager(engine.GetProxyManager())
botAdapter = feishuBot
log.Printf("Registered %s bot adapter (WebSocket long connection)", botType)
logger.Infof("Registered %s bot adapter (WebSocket long connection)", botType)

case "dingtalk":
dingtalkBot := bot.NewDingTalkBot(botConfig.AppID, botConfig.AppSecret)
dingtalkBot.SetProxyManager(engine.GetProxyManager())
botAdapter = dingtalkBot
log.Printf("Registered %s bot adapter (WebSocket long connection)", botType)
logger.Infof("Registered %s bot adapter (WebSocket long connection)", botType)

case "telegram":
telegramBot := bot.NewTelegramBot(botConfig.Token)
if botConfig.ParseMode != "" {
telegramBot.SetParseMode(botConfig.ParseMode)
}
telegramBot.SetProxyManager(engine.GetProxyManager())
botAdapter = telegramBot
log.Printf("Registered %s bot adapter (long polling)", botType)
logger.Infof("Registered %s bot adapter (long polling, parse_mode: %s)", botType, botConfig.ParseMode)

case "qq":
qqBot := bot.NewQQBot(botConfig.AppID, botConfig.AppSecret)
qqBot.SetProxyManager(engine.GetProxyManager())
botAdapter = qqBot
log.Printf("Registered %s bot adapter (WebSocket long connection)", botType)
logger.Infof("Registered %s bot adapter (WebSocket long connection)", botType)

default:
log.Printf("Warning: Bot type '%s' not implemented yet", botType)
logger.Warnf("Warning: Bot type '%s' not implemented yet", botType)
continue
}

Expand Down
3 changes: 3 additions & 0 deletions configs/config.full.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ bots:
# TIP: Use environment variables for better security
# See docs above on how to use ENV variables in config
token: "123456789:ABCdefGHIjklMNOpqrsTUVwxyz" # Replace with your bot token
# Optional: Message formatting mode (Markdown, MarkdownV2, HTML)
# Use "Markdown" or "HTML" to enable rich text and code blocks for tables
parse_mode: "Markdown"
# Optional: Bot-level proxy (overrides global proxy)
# proxy:
# enabled: true # Set to true to use this proxy instead of global
Expand Down
10 changes: 10 additions & 0 deletions debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main
import (
"fmt"
"github.com/keepmind9/clibot/internal/bot"
)
func main() {
md := "- Item 1\n- Item 2\n - Nested 1\n - Nested 2\n- Item 3"
res := bot.ConvertMarkdownToTelegramHTML(md)
fmt.Printf("RES: %q\n", res)
}
Binary file added debug_out.txt
Binary file not shown.
Binary file added gemini_help.txt
Binary file not shown.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ require (
)

require (
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/yuin/goldmark v1.7.16 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.41.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/coder/acp-go-sdk v0.6.3 h1:LsXQytehdjKIYJnoVWON/nf7mqbiarnyuyE3rrjBsXQ=
github.com/coder/acp-go-sdk v0.6.3/go.mod h1:yKzM/3R9uELp4+nBAwwtkS0aN1FOFjo11CNPy37yFko=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
Expand All @@ -22,6 +24,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/larksuite/oapi-sdk-go/v3 v3.5.3 h1:xvf8Dv29kBXC5/DNDCLhHkAFW8l/0LlQJimO5Zn+JUk=
github.com/larksuite/oapi-sdk-go/v3 v3.5.3/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 h1:Lb/Uzkiw2Ugt2Xf03J5wmv81PdkYOiWbI8CNBi1boC8=
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -37,6 +41,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
Expand Down
6 changes: 6 additions & 0 deletions internal/bot/dingtalk.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ func (d *DingTalkBot) SendMessage(conversationID, message string) error {
// The webhook URL is received in each incoming message but needs to be stored
// and reused for replies. This implementation limitation is tracked at:
// https://github.com/keepmind9/clibot/issues/125
message = WrapTablesInCodeBlocks(message)
logger.WithFields(logrus.Fields{
"conversation_id": conversationID,
"message_length": len(message),
Expand Down Expand Up @@ -188,6 +189,11 @@ func (d *DingTalkBot) SetMessageHandler(handler func(BotMessage)) {
d.messageHandler = handler
}

// GetBotUsername returns the DingTalk bot's identifier (ClientID)
func (d *DingTalkBot) GetBotUsername() string {
return d.clientID
}

// GetMessageHandler gets the message handler in a thread-safe manner
func (d *DingTalkBot) GetMessageHandler() func(BotMessage) {
d.mu.RLock()
Expand Down
13 changes: 13 additions & 0 deletions internal/bot/discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func (d *DiscordBot) SendMessage(channel, message string) error {
message = "..." + message[len(message)-maxDiscordLength+3:]
}

message = WrapTablesInCodeBlocks(message)
_, err := session.ChannelMessageSend(targetChannel, message)
if err != nil {
logger.WithFields(logrus.Fields{
Expand Down Expand Up @@ -200,6 +201,18 @@ func (d *DiscordBot) SetMessageHandler(handler func(BotMessage)) {
d.messageHandler = handler
}

// GetBotUsername returns the Discord bot's username
func (d *DiscordBot) GetBotUsername() string {
d.mu.RLock()
defer d.mu.RUnlock()
if d.session != nil {
if sess, ok := d.session.(*discordgo.Session); ok && sess.State != nil && sess.State.User != nil {
return sess.State.User.Username
}
}
return ""
}

// GetMessageHandler gets the message handler in a thread-safe manner
func (d *DiscordBot) GetMessageHandler() func(BotMessage) {
d.mu.RLock()
Expand Down
6 changes: 6 additions & 0 deletions internal/bot/feishu.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ func (f *FeishuBot) SendMessage(chatID, message string) error {

// Create message request body
// For text messages, content format: {"text":"actual content"}
message = WrapTablesInCodeBlocks(message)
contentJSON := fmt.Sprintf(`{"text":"%s"}`, escapeJSONString(message))

body := larkim.NewCreateMessageReqBodyBuilder().
Expand Down Expand Up @@ -299,6 +300,11 @@ func (f *FeishuBot) SetMessageHandler(handler func(BotMessage)) {
f.messageHandler = handler
}

// GetBotUsername returns the Feishu bot's identifier (AppID)
func (f *FeishuBot) GetBotUsername() string {
return f.appID
}

// GetMessageHandler gets the message handler in a thread-safe manner
func (f *FeishuBot) GetMessageHandler() func(BotMessage) {
f.mu.RLock()
Expand Down
8 changes: 8 additions & 0 deletions internal/bot/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ func (d *DefaultTypingIndicator) RemoveTypingIndicator(messageID string) error {
return nil
}

// GetBotUsername returns an empty string (not supported by default)
func (d *DefaultTypingIndicator) GetBotUsername() string {
return ""
}

// BotAdapter defines the interface for bot adapters
type BotAdapter interface {
// Start starts the bot, establishes connection and begins listening for messages
Expand All @@ -90,6 +95,9 @@ type BotAdapter interface {
// SetProxyManager sets the proxy manager for the bot
SetProxyManager(mgr proxy.Manager)

// GetBotUsername returns the bot's username on the platform
GetBotUsername() string

// Stop stops the bot and cleans up resources
Stop() error
}
Expand Down
Loading