remoteagent

package
v0.260806.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MPL-2.0 Imports: 34 Imported by: 0

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

View Source
const (
	DefaultMaxImageSize = 25 * 1024 * 1024 // 25MB
	DefaultMaxDocSize   = 50 * 1024 * 1024 // 50MB
)
View Source
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

View Source
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

View Source
const (
	MsgProcessing     = "Processing..."
	MsgTaskDone       = "Task done"
	MsgContinueOrHelp = "Continue or /help."
)

Status messages

View Source
const SeparatorLine = "───────────────"

SeparatorLine visually splits a message body from its footer.

Variables

View Source
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

func BuildFooter(agentType, projectPath string) string

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

func EnsureContext(t testing.TB) context.Context

EnsureContext provides a context that propagates either through t.Context() (Go 1.24+) or a fresh background context.

func ExpandPath

func ExpandPath(path string) (string, error)

ExpandPath expands ~ and environment variables in a path

func ExpandPathFrom

func ExpandPathFrom(path, baseDir string) (string, error)

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

func GetAgentDisplayName(agentType string) string

GetAgentDisplayName returns the short display name for an agent type

func GetAgentIcon

func GetAgentIcon(agentType string) string

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 ShortenPath

func ShortenPath(path string) string

ShortenPath shortens a path for display

func ValidateProjectPath

func ValidateProjectPath(path string) error

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 (h *BotHandler) HandleMessage(msg imbot.Message, platform imbot.Platform, botUUID string)

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

	// BuildReplyFooter returns a compact footer (agent + project path) that
	// 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

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

func NewFileStoreWithProxy(proxyURL string) (*FileStore, error)

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

func (s *FileStore) GetDownloadDir(projectPath string) string

GetDownloadDir returns the .download directory for a project

func (*FileStore) IsAllowedSize

func (s *FileStore) IsAllowedSize(mimeType string, size int64) bool

IsAllowedSize checks if the size is within limits for the mime type

func (*FileStore) IsAllowedType

func (s *FileStore) IsAllowedType(mimeType string) bool

IsAllowedType checks if the mime type is allowed

func (*FileStore) SetTelegramToken

func (s *FileStore) SetTelegramToken(token string)

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

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

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

type Steerable interface {
	Steer(text string) bool
}

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL