Documentation
¶
Overview ¶
Package remoteagent implements the "remote_agent" bot purpose: controlling Claude Code (@cc) and the SmartGuide agent (@tb) from a chat. It plugs into the bot host (internal/remote_control/bot) as a Consumer; the host owns the lifecycle, the shared channel prompter, and prompt-reply routing, while this package owns everything agent-specific — the inbound BotHandler, slash commands, agent routing/executors, and the streaming chat renderer.
Index ¶
- Constants
- Variables
- func BuildCustomPathPrompt() string
- func BuildFooter(agentType, projectPath string) string
- func EnsureContext(t testing.TB) context.Context
- func ExpandPath(path string) (string, error)
- func ExpandPathFrom(path, baseDir string) (string, error)
- func GetAgentDisplayName(agentType string) string
- func GetAgentIcon(agentType string) string
- func NewConsumer(sessionMgr *session.Manager, agentService *agentboot.AgentService, ...) bot2.Consumer
- func RegisterBuiltinCommands(registry *imbot.CommandRegistry, botHandler BotHandlerAdapter) error
- func ShortenPath(path string) string
- func ValidateProjectPath(path string) error
- type AgentExecutor
- type AgentRouter
- type BotHandler
- func (h *BotHandler) GetCommandRegistry() *imbot.CommandRegistry
- func (h *BotHandler) GetVerbose(chatID string) bool
- func (h *BotHandler) HandleMessage(msg imbot.Message, platform imbot.Platform, botUUID string)
- func (h *BotHandler) InitCommandRegistry() error
- func (h *BotHandler) SendFile(ctx context.Context, hCtx HandlerContext, filePath, caption string) error
- func (h *BotHandler) SendText(hCtx HandlerContext, text string)
- func (h *BotHandler) SetVerbose(chatID string, verbose bool)
- func (h *BotHandler) VerifyAndPair(botUUID, chatID, senderID, platform, code string) error
- type BotHandlerAdapter
- type ClaudeCodeExecutor
- type ExecutionRequest
- type ExecutorDependencies
- type FileStore
- func (s *FileStore) DownloadFile(ctx context.Context, projectPath, url, mimeType string) (*StoredFile, error)
- func (s *FileStore) GetDownloadDir(projectPath string) string
- func (s *FileStore) IsAllowedSize(mimeType string, size int64) bool
- func (s *FileStore) IsAllowedType(mimeType string) bool
- func (s *FileStore) SetTelegramToken(token string)
- type HandlerContext
- type PreparedRequest
- type ResponseMeta
- type ResumableSession
- type SessionInfo
- type SmartGuideCompletionCallback
- type SmartGuideExecutor
- type Steerable
- type StoredFile
- type TelegramFile
- type TestBootOptions
- type TestHarness
Constants ¶
const ( DefaultMaxImageSize = 25 * 1024 * 1024 // 25MB DefaultMaxDocSize = 50 * 1024 * 1024 // 50MB )
const ( // Icons IconProject = "📁" // Project/folder IconAgentTB = "🎯" // Tingly-Box agent (@tb) IconAgentCC = "💬" // Claude Code agent (@cc) IconDone = "✅" // Task completed IconError = "❌" // Error IconProcess = "⏳" // Processing IconTool = "🔧" // Tool call IconToolResult = "↳" // Tool result IconThinking = "💭" // Model reasoning (not an answer) IconSteer = "↪" // Follow-up folded into the running task )
Output format constants for bot messages Centralized for easy customization and i18n support
const ( AgentNameTB = "@tb" // Tingly-Box short name AgentNameCC = "@cc" // Claude Code short name AgentNameTinglyBox = "tingly-box" // mirrors db.DefaultChatAgent; the single source of truth lives in internal/data/db, kept here as a literal so this package does not import db AgentNameClaude = "claude" )
Agent display names
const ( MsgProcessing = "Processing..." MsgTaskDone = "Task done" MsgContinueOrHelp = "Continue or /help." )
Status messages
const SeparatorLine = "───────────────"
SeparatorLine visually splits a message body from its footer.
Variables ¶
var AllowedMIMETypes = map[string]string{
"image/jpeg": "image",
"image/png": "image",
"image/gif": "image",
"image/webp": "image",
"application/pdf": "document",
"text/plain": "document",
"text/markdown": "document",
"text/csv": "document",
"application/msword": "document",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "document",
"application/vnd.ms-excel": "document",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "document",
}
AllowedMIMETypes lists supported file types
Functions ¶
func BuildCustomPathPrompt ¶
func BuildCustomPathPrompt() string
BuildCustomPathPrompt returns the text for custom path input prompt
func BuildFooter ¶
BuildFooter creates a compact footer line with agent and path info. Format: separator + agent line + path line. Either part is omitted when the corresponding value is empty, and an empty footer is returned when both are empty so callers don't print a stray separator.
func EnsureContext ¶
EnsureContext provides a context that propagates either through t.Context() (Go 1.24+) or a fresh background context.
func ExpandPath ¶
ExpandPath expands ~ and environment variables in a path
func ExpandPathFrom ¶
ExpandPathFrom expands a user-provided path with the given baseDir as the reference for relative paths. ~/ and absolute paths are handled as in ExpandPath. When path is relative and baseDir is non-empty, it is joined with baseDir; otherwise it falls back to filepath.Abs (process cwd).
func GetAgentDisplayName ¶
GetAgentDisplayName returns the short display name for an agent type
func GetAgentIcon ¶
GetAgentIcon returns the icon for an agent type
func NewConsumer ¶
func NewConsumer( sessionMgr *session.Manager, agentService *agentboot.AgentService, tbClient tbclient.TBClient, store bot2.SettingsStore, ) bot2.Consumer
NewConsumer builds the consumer that binds a bot to the remote-agent purpose. tbClient and store may be nil (standalone / test use): SmartGuide falls back to Claude Code and dynamic settings refresh is skipped.
func RegisterBuiltinCommands ¶
func RegisterBuiltinCommands(registry *imbot.CommandRegistry, botHandler BotHandlerAdapter) error
RegisterBuiltinCommands registers all built-in commands to the registry.
func ValidateProjectPath ¶
ValidateProjectPath validates that a path is a valid project directory
Types ¶
type AgentExecutor ¶
type AgentExecutor interface {
// Execute processes a prepared request. Streaming output and completion
// cards go straight to the chat; the caller only needs the error.
Execute(ctx context.Context, req PreparedRequest) error
// GetAgentType returns the agent type identifier
GetAgentType() agentboot.AgentType
}
AgentExecutor defines the interface for executing agent requests. Each agent type (Claude Code, Smart Guide) implements this interface.
type AgentRouter ¶
type AgentRouter struct {
// contains filtered or unexported fields
}
AgentRouter routes execution requests to the appropriate agent executor. It resolves common concerns (project path, session, meta, cancel context) once, then delegates to the specific executor.
func NewAgentRouter ¶
func NewAgentRouter(deps *ExecutorDependencies) *AgentRouter
NewAgentRouter creates a new agent router with the given dependencies
func (*AgentRouter) Execute ¶
func (r *AgentRouter) Execute(ctx context.Context, agentType agentboot.AgentType, req ExecutionRequest) error
Execute routes the execution request to the appropriate agent executor. It resolves project path, session, and builds shared *ResponseMeta before delegating.
func (*AgentRouter) RegisterExecutor ¶
func (r *AgentRouter) RegisterExecutor(executor AgentExecutor)
RegisterExecutor registers an agent executor
type BotHandler ¶
type BotHandler struct {
// contains filtered or unexported fields
}
BotHandler encapsulates all bot message handling logic and dependencies
func NewBotHandler ¶
func NewBotHandler( ctx context.Context, botSetting bot2.BotSetting, chatStore bot2.ChatStoreInterface, sessionMgr *session.Manager, agentService *agentboot.AgentService, directoryBrowser *feature.DirectoryBrowser, manager *imbot.Manager, prompter *imchannel.IMPrompter, tbClient tbclient.TBClient, pairing *bot2.PairingManager, store bot2.SettingsStore, ) *BotHandler
func (*BotHandler) GetCommandRegistry ¶
func (h *BotHandler) GetCommandRegistry() *imbot.CommandRegistry
GetCommandRegistry returns the command registry.
func (*BotHandler) GetVerbose ¶
func (h *BotHandler) GetVerbose(chatID string) bool
func (*BotHandler) HandleMessage ¶
func (*BotHandler) InitCommandRegistry ¶
func (h *BotHandler) InitCommandRegistry() error
InitCommandRegistry initializes the command registry with built-in commands.
func (*BotHandler) SendFile ¶
func (h *BotHandler) SendFile(ctx context.Context, hCtx HandlerContext, filePath, caption string) error
SendFile sends a local file to the user via the IM bot. The file is read from disk and sent as a MediaAttachment. caption may be empty.
func (*BotHandler) SendText ¶
func (h *BotHandler) SendText(hCtx HandlerContext, text string)
func (*BotHandler) SetVerbose ¶
func (h *BotHandler) SetVerbose(chatID string, verbose bool)
SetVerbose sets the verbose mode for a chat
func (*BotHandler) VerifyAndPair ¶
func (h *BotHandler) VerifyAndPair(botUUID, chatID, senderID, platform, code string) error
VerifyAndPair runs the pairing-code check and, on success, persists the binding in the chat store. It is invoked by the /bind command handler.
type BotHandlerAdapter ¶
type BotHandlerAdapter interface {
// SendText sends a text message to a chat
SendText(chatID, text string) error
// GetProjectPath gets the current project path for a chat
GetProjectPath(chatID string) (string, error)
// SetProjectPath sets the project path for a chat
SetProjectPath(chatID, path string) error
// GetSession gets session info
GetSession(chatID, agentType, projectPath string) (*SessionInfo, error)
// FindOrCreateSession finds an existing session or creates a new one
FindOrCreateSession(chatID, agentType, projectPath string) (*SessionInfo, error)
// UpdatePermissionMode updates the permission mode for a session
UpdatePermissionMode(sessionID, mode string) error
// ClearSession clears the current agent's session for the chat
ClearSession(chatID string) error
// StopExecution cancels a running execution, returns true if one was running
StopExecution(chatID string) bool
// GetCurrentAgent gets the current agent for a chat
GetCurrentAgent(chatID string) (string, error)
// SetVerbose sets verbose mode for a chat
SetVerbose(chatID string, enabled bool)
// GetVerbose gets verbose mode for a chat
GetVerbose(chatID string) bool
// IsWhitelisted checks if a group is whitelisted
IsWhitelisted(groupID string) bool
// AddToWhitelist adds a group to whitelist
AddToWhitelist(groupID, platform, userID string) error
// GetBashCwd gets the bash working directory
GetBashCwd(chatID string) (string, error)
// SetBashCwd sets the bash working directory
SetBashCwd(chatID, path string) error
// GetBashAllowlist returns the configured bash allowlist
GetBashAllowlist() map[string]struct{}
// BuildHelpText renders the registry's command list for /help.
BuildHelpText(isDirect bool) string
// ListChatProjectPaths lists the MRU per-chat project-path history.
ListChatProjectPaths(chatID string) ([]string, error)
// VerifyAndPair verifies a one-time pairing code and, on success, records
// the chat as paired with the bot. Implementations should also emit the
// matching audit events (success / failure).
VerifyAndPair(botUUID, chatID, senderID, platform, code string) error
// command replies append for context continuity. Returns an empty string
// when neither agent nor project path is resolvable for the chat.
BuildReplyFooter(chatID, platform string) string
// ListResumableSessions lists the most recent Claude sessions on disk for
// the given project, newest first, capped to limit.
ListResumableSessions(projectPath string, limit int) ([]ResumableSession, error)
// PrepareResume binds the given Claude session_id as the next session for
// (chatID, agentType, projectPath). The next user message will be sent with
// --resume <sessionID>.
PrepareResume(chatID, agentType, projectPath, sessionID string) error
// RememberResumeListing stores the session IDs presented to the user so
// /resume <n> can resolve back to them. Order is the same as the displayed
// list (1-indexed externally).
RememberResumeListing(chatID string, sessionIDs []string)
// RecallResumeListing returns the most recently displayed session IDs, in
// display order. Returns nil if no listing was remembered.
RecallResumeListing(chatID string) []string
}
BotHandlerAdapter provides methods needed by command handlers. This allows commands to interact with the bot without direct coupling.
func NewBotHandlerAdapter ¶
func NewBotHandlerAdapter(handler *BotHandler) BotHandlerAdapter
NewBotHandlerAdapter creates a new adapter for the given handler.
type ClaudeCodeExecutor ¶
type ClaudeCodeExecutor struct {
// contains filtered or unexported fields
}
ClaudeCodeExecutor executes messages through the Claude Code agent.
It consumes the agentboot.ExecutionHandle returned by Agent.Execute directly, dispatching MessageEvents to the streaming chat writer and routing ApprovalRequestEvent / AskRequestEvent to IMPrompter.
func NewClaudeCodeExecutor ¶
func NewClaudeCodeExecutor(deps *ExecutorDependencies) *ClaudeCodeExecutor
NewClaudeCodeExecutor creates a new Claude Code executor.
func (*ClaudeCodeExecutor) Execute ¶
func (e *ClaudeCodeExecutor) Execute(ctx context.Context, req PreparedRequest) error
Execute processes a user message through Claude Code.
func (*ClaudeCodeExecutor) GetAgentType ¶
func (e *ClaudeCodeExecutor) GetAgentType() agentboot.AgentType
GetAgentType returns the agent type identifier.
type ExecutionRequest ¶
type ExecutionRequest struct {
HCtx HandlerContext
Text string
ProjectPath string // optional override
ReplyToMessageID string
}
ExecutionRequest contains caller-provided parameters (from bot handler layer).
type ExecutorDependencies ¶
type ExecutorDependencies struct {
// GetBotSetting dynamically retrieves the current bot settings from the store.
// This ensures that any configuration changes (provider, model, etc.) are reflected
// immediately without requiring a bot restart.
GetBotSetting func() (bot.BotSetting, error)
ChatStore bot.ChatStoreInterface
SessionMgr *session.Manager
AgentService *agentboot.AgentService
IMPrompter *imchannel.IMPrompter
FileStore *FileStore
TBClient tbclient.TBClient
TBSessionStore *smart_guide.SessionStore
Executions *executionRegistry
SendText func(hCtx HandlerContext, text string)
SendTextWithReply func(hCtx HandlerContext, text string, replyTo string)
SendFile func(hCtx HandlerContext, filePath, caption string) error
NewStreamingMessageHandler func(hCtx HandlerContext) *streamingMessageHandler
}
ExecutorDependencies holds shared dependencies for agent executors and router.
func (*ExecutorDependencies) GetBotSettingOrCache ¶
func (d *ExecutorDependencies) GetBotSettingOrCache() bot.BotSetting
GetBotSettingOrCache returns the current bot setting. If dynamic lookup fails, returns an empty setting.
func (*ExecutorDependencies) ResolveDefaultProjectPath ¶
func (d *ExecutorDependencies) ResolveDefaultProjectPath() string
ResolveDefaultProjectPath returns the default project path from bot settings.
type FileStore ¶
type FileStore struct {
// contains filtered or unexported fields
}
FileStore handles project-based file storage for bot media
func NewFileStore ¶
func NewFileStore() *FileStore
NewFileStore creates a new file store with default limits
func NewFileStoreWithProxy ¶
NewFileStoreWithProxy creates a new file store with proxy support
func (*FileStore) DownloadFile ¶
func (s *FileStore) DownloadFile(ctx context.Context, projectPath, url, mimeType string) (*StoredFile, error)
DownloadFile downloads a file from a URL to the project's .download directory Returns an error if file size exceeds limits
func (*FileStore) GetDownloadDir ¶
GetDownloadDir returns the .download directory for a project
func (*FileStore) IsAllowedSize ¶
IsAllowedSize checks if the size is within limits for the mime type
func (*FileStore) IsAllowedType ¶
IsAllowedType checks if the mime type is allowed
func (*FileStore) SetTelegramToken ¶
SetTelegramToken sets the Telegram bot token for resolving file URLs
type HandlerContext ¶
type HandlerContext struct {
Bot imbot.Bot
BotUUID string
ChatID string
SenderID string
MessageID string
Platform imbot.Platform
Message imbot.Message
}
HandlerContext contains per-message context data
func (*HandlerContext) IsDirect ¶
func (c *HandlerContext) IsDirect() bool
func (*HandlerContext) Text ¶
func (c *HandlerContext) Text() string
type PreparedRequest ¶
type PreparedRequest struct {
HCtx HandlerContext
Text string
ProjectPath string // fully resolved: override > ChatStore > default
Meta *ResponseMeta // shared pointer, created by router
SessionID string // resolved session ID (chatID for SmartGuide)
IsNewSession bool // whether session was just created
PermissionMode string // resolved from session (Claude Code)
ReplyTo string
}
PreparedRequest is the fully-resolved request built by AgentRouter. All executors receive this — shared *ResponseMeta ensures path changes propagate.
type ResponseMeta ¶
type ResponseMeta struct {
ProjectPath string
AgentType string // Current agent identifier (e.g., "tingly-box", "claude")
}
ResponseMeta carries the two values response footers render: the acting agent and the chat's project path. It is shared by pointer between the router, the executors, and the SmartGuide completion callback so a mid-execution project change is reflected in the closing footer.
type ResumableSession ¶
type ResumableSession struct {
SessionID string
StartTime time.Time
EndTime time.Time
NumTurns int
Status string
FirstMessage string
}
ResumableSession is the per-row info returned by ListResumableSessions. Channel-neutral so command code can render either compact text or buttons.
type SessionInfo ¶
type SessionInfo struct {
ID string
Status string
Project string
Request string
Error string
PermissionMode string
LastActivity time.Time
}
SessionInfo holds session information.
type SmartGuideCompletionCallback ¶
type SmartGuideCompletionCallback struct {
// contains filtered or unexported fields
}
SmartGuideCompletionCallback handles completion events for SmartGuide agent It saves messages to session, updates project path if changed, and sends response + action keyboard
func (*SmartGuideCompletionCallback) OnComplete ¶
func (c *SmartGuideCompletionCallback) OnComplete(result *smart_guide2.CompletionResult)
OnComplete handles the smart-guide completion signal.
type SmartGuideExecutor ¶
type SmartGuideExecutor struct {
// contains filtered or unexported fields
}
SmartGuideExecutor executes messages through Smart Guide (Tingly Box) agent
func NewSmartGuideExecutor ¶
func NewSmartGuideExecutor(deps *ExecutorDependencies) *SmartGuideExecutor
NewSmartGuideExecutor creates a new Smart Guide executor
func (*SmartGuideExecutor) Execute ¶
func (e *SmartGuideExecutor) Execute(ctx context.Context, req PreparedRequest) error
Execute processes a user message through Smart Guide
func (*SmartGuideExecutor) GetAgentType ¶
func (e *SmartGuideExecutor) GetAgentType() agentboot.AgentType
GetAgentType returns the agent type identifier
type Steerable ¶
Steerable is a running execution that can take a message mid-run instead of making the user wait for it to finish.
type StoredFile ¶
type StoredFile struct {
Path string // Full path: {projectPath}/.agent/{filename}
RelPath string // Relative path for agent: .agent/{filename}
URL string // Original URL
Filename string
Size int64
MimeType string
}
StoredFile represents a stored file
type TelegramFile ¶
type TelegramFile struct {
Ok bool `json:"ok"`
Result struct {
FileID string `json:"file_id"`
FileSize int `json:"file_size"`
FilePath string `json:"file_path"`
} `json:"result"`
}
TelegramFile represents the response from Telegram's getFile API
type TestBootOptions ¶
type TestBootOptions struct {
// DataDir overrides the chat-store directory (default: t.TempDir()).
DataDir string
// FixtureScript, when non-nil, registers a Claude agent backed by a
// fixture.Factory(script). The fixture replaces the legacy mockagent —
// tests now drive the real claude.Driver + claude.Transport + Runner
// pipeline against scripted wire-format output.
//
// When nil (default), no Claude agent is registered and tests that
// depend on agent execution must register their own.
FixtureScript fixture.Script
}
TestBootOptions tweaks BootForTest defaults. All fields are optional.
type TestHarness ¶
type TestHarness struct {
Setting bot2.BotSetting
Handler *BotHandler
ChatStore bot2.ChatStoreInterface
SessionMgr *session.Manager
AgentService *agentboot.AgentService
Pairing *bot2.PairingManager
DataDir string
Manager *imbot.Manager
// contains filtered or unexported fields
}
TestHarness wires the production BotHandler against a test imbot.Manager (typically backed by the tingly platform). It owns the support infrastructure — chat store, session manager, agentboot, pairing — and exposes them so tests can drive state directly.
Construction:
env := testenv.NewTestEnv(t)
uuid := env.BotUUID() // creates a tingly bot in env.Manager()
harness := bot.BootForTest(t, env.Manager(), bot.BotSetting{
UUID: uuid,
Platform: "tingly",
Enabled: true,
})
require.NoError(t, env.Manager().Start(env.Context()))
Tests then drive the bot through the testenv chat helpers.
func BootForTest ¶
func BootForTest(t *testing.T, manager *imbot.Manager, setting bot2.BotSetting, opts ...TestBootOptions) *TestHarness
BootForTest spins up a production BotHandler against the given imbot.Manager. It assumes the Manager already has a bot registered for setting.UUID (the tingly testenv arranges this via AddTinglyBotWithUUID when env.BotUUID() is called).
The harness registers the BotHandler.HandleMessage callback on the Manager. Callers must Start the Manager themselves — keeping that step in the test makes it explicit when inbound messages start flowing.
func (*TestHarness) MarkChatPaired ¶
func (h *TestHarness) MarkChatPaired(chatID, senderID string)
MarkChatPaired records a pairing for the harness's bot via the same production API path that VerifyAndPair uses. Tests focused on post-pairing behavior can skip the /bind handshake without bypassing the real persistence path — exercising any future bug in SetPaired.
func (*TestHarness) MintPairingCode ¶
func (h *TestHarness) MintPairingCode() (code string, expiresAt time.Time)
MintPairingCode mints a fresh pairing code for the harness's bot. Tests that exercise the pairing-required path use this to obtain the code the user must send via /bind.
func (*TestHarness) SetCurrentAgent ¶
func (h *TestHarness) SetCurrentAgent(chatID, agentType string)
SetCurrentAgent updates the current-agent binding for a chat through the same production path the @cc/@tb handoff uses. Going through chatStore.SetCurrentAgent (rather than mutating Chat directly) keeps the harness honest: any regression in the persistence path — e.g. a silent no-op on a missing chat row — surfaces as a test failure.
func (*TestHarness) WhitelistGroup ¶
func (h *TestHarness) WhitelistGroup(chatID, ownerID string)
WhitelistGroup adds a group chat to the bot's whitelist (required for the bot to respond to group messages).
Source Files
¶
- agent_executor.go
- agent_router.go
- callback.go
- command.go
- command_adapter.go
- command_dispatch.go
- consumer.go
- exec_registry.go
- executor_claude.go
- executor_smartguide.go
- file_send.go
- file_store.go
- handler.go
- handler_bind.go
- handler_constructor.go
- handler_message.go
- handler_pair.go
- handler_send.go
- handler_verbose.go
- handoff.go
- output.go
- stream.go
- testharness.go
- types.go
- util.go