diff --git a/cmd/memoh/serve.go b/cmd/memoh/serve.go index 5c6da830..ff8ece39 100644 --- a/cmd/memoh/serve.go +++ b/cmd/memoh/serve.go @@ -25,6 +25,7 @@ import ( "github.com/memohai/memoh/internal/bind" "github.com/memohai/memoh/internal/boot" "github.com/memohai/memoh/internal/bots" + "github.com/memohai/memoh/internal/browsercontexts" agentruntime "github.com/memohai/memoh/internal/bun/runtime" "github.com/memohai/memoh/internal/channel" "github.com/memohai/memoh/internal/channel/adapters/discord" @@ -48,7 +49,6 @@ import ( emailmailgun "github.com/memohai/memoh/internal/email/adapters/mailgun" "github.com/memohai/memoh/internal/handlers" "github.com/memohai/memoh/internal/healthcheck" - "github.com/memohai/memoh/internal/browsercontexts" channelchecker "github.com/memohai/memoh/internal/healthcheck/checkers/channel" mcpchecker "github.com/memohai/memoh/internal/healthcheck/checkers/mcp" "github.com/memohai/memoh/internal/heartbeat" diff --git a/internal/channel/adapters/feishu/quoted_message.go b/internal/channel/adapters/feishu/quoted_message.go index aa2f1b43..d3c59955 100644 --- a/internal/channel/adapters/feishu/quoted_message.go +++ b/internal/channel/adapters/feishu/quoted_message.go @@ -57,7 +57,7 @@ func (a *FeishuAdapter) enrichQuotedMessage(ctx context.Context, cfg channel.Cha a.logger.Debug("feishu quoted message fetch empty", slog.String("parent_id", parentID), slog.Int("code", code), - slog.String("msg", respMsg), + slog.String("response_msg", respMsg), ) } return diff --git a/internal/channel/inbound/channel.go b/internal/channel/inbound/channel.go index 2fcbf3e1..fa73479d 100644 --- a/internal/channel/inbound/channel.go +++ b/internal/channel/inbound/channel.go @@ -19,6 +19,7 @@ import ( "github.com/memohai/memoh/internal/auth" "github.com/memohai/memoh/internal/channel" "github.com/memohai/memoh/internal/channel/route" + "github.com/memohai/memoh/internal/command" "github.com/memohai/memoh/internal/conversation" "github.com/memohai/memoh/internal/conversation/flow" "github.com/memohai/memoh/internal/inbox" @@ -55,18 +56,19 @@ type mediaIngestor interface { // ChannelInboundProcessor routes channel inbound messages to the chat gateway. type ChannelInboundProcessor struct { - runner flow.Runner - routeResolver RouteResolver - message messagepkg.Writer - mediaService mediaIngestor - reactor channelReactor - inboxService *inbox.Service - registry *channel.Registry - logger *slog.Logger - jwtSecret string - tokenTTL time.Duration - identity *IdentityResolver - observer channel.StreamObserver + runner flow.Runner + routeResolver RouteResolver + message messagepkg.Writer + mediaService mediaIngestor + reactor channelReactor + inboxService *inbox.Service + commandHandler *command.Handler + registry *channel.Registry + logger *slog.Logger + jwtSecret string + tokenTTL time.Duration + identity *IdentityResolver + observer channel.StreamObserver } // NewChannelInboundProcessor creates a processor with channel identity-based resolution. @@ -146,6 +148,15 @@ func (p *ChannelInboundProcessor) SetInboxService(service *inbox.Service) { p.inboxService = service } +// SetCommandHandler configures the slash command handler for intercepting +// /command messages before they reach the LLM. +func (p *ChannelInboundProcessor) SetCommandHandler(handler *command.Handler) { + if p == nil { + return + } + p.commandHandler = handler +} + // HandleInbound processes an inbound channel message through identity resolution and chat gateway. func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel.ChannelConfig, msg channel.InboundMessage, sender channel.StreamReplySender) error { if p.runner == nil { @@ -195,6 +206,19 @@ func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel } identity := state.Identity + + // Intercept slash commands before they reach the LLM. + if p.commandHandler != nil && p.commandHandler.IsCommand(text) { + reply, err := p.commandHandler.Execute(ctx, strings.TrimSpace(identity.BotID), strings.TrimSpace(identity.ChannelIdentityID), text) + if err != nil { + reply = "Error: " + err.Error() + } + return sender.Send(ctx, channel.OutboundMessage{ + Target: strings.TrimSpace(msg.ReplyTarget), + Message: channel.Message{Text: reply}, + }) + } + resolvedAttachments := p.ingestInboundAttachments(ctx, cfg, msg, strings.TrimSpace(identity.BotID), msg.Message.Attachments) attachments := mapChannelToChatAttachments(resolvedAttachments) text = buildInboundQuery(msg.Message, attachments) @@ -837,7 +861,6 @@ func buildChannelMessage(output conversation.AssistantOutput, capabilities chann return msg } - func contentPartHasValue(part conversation.ContentPart) bool { if strings.TrimSpace(part.Text) != "" { return true diff --git a/internal/command/email_cmd.go b/internal/command/email_cmd.go index 4d9fc6e3..951f70c2 100644 --- a/internal/command/email_cmd.go +++ b/internal/command/email_cmd.go @@ -1,7 +1,6 @@ package command import ( - "fmt" "strings" ) @@ -91,5 +90,5 @@ func buildPermString(read, write, del bool) string { if len(parts) == 0 { return "none" } - return fmt.Sprintf("%s", strings.Join(parts, ", ")) + return strings.Join(parts, ", ") } diff --git a/internal/command/formatter.go b/internal/command/formatter.go index 43d3f211..ec813aca 100644 --- a/internal/command/formatter.go +++ b/internal/command/formatter.go @@ -11,13 +11,13 @@ import ( // // Example output: // -// - mybot -// Description: A helpful assistant -// ID: abc123 +// - mybot +// Description: A helpful assistant +// ID: abc123 // -// - another -// Description: Something else -// ID: def456 +// - another +// Description: Something else +// ID: def456 func formatItems(items [][]kv) string { if len(items) == 0 { return "" @@ -42,8 +42,8 @@ func formatItems(items [][]kv) string { // // Example output: // -// - ID: abc123 -// - Name: mybot +// - ID: abc123 +// - Name: mybot func formatKV(pairs []kv) string { var b strings.Builder for _, p := range pairs { diff --git a/internal/command/handler.go b/internal/command/handler.go index 52ecbb0d..28cb1e38 100644 --- a/internal/command/handler.go +++ b/internal/command/handler.go @@ -49,17 +49,17 @@ type Handler struct { mcpConnService *mcp.ConnectionService inboxService *inbox.Service - modelsService *models.Service - providersService *providers.Service - memProvService *memprovider.Service - searchProvService *searchproviders.Service - browserCtxService *browsercontexts.Service - emailService *emailpkg.Service + modelsService *models.Service + providersService *providers.Service + memProvService *memprovider.Service + searchProvService *searchproviders.Service + browserCtxService *browsercontexts.Service + emailService *emailpkg.Service emailOutboxService *emailpkg.OutboxService - heartbeatService *heartbeat.Service - queries *dbsqlc.Queries - skillLoader SkillLoader - containerFS ContainerFS + heartbeatService *heartbeat.Service + queries *dbsqlc.Queries + skillLoader SkillLoader + containerFS ContainerFS logger *slog.Logger } diff --git a/internal/command/handler_test.go b/internal/command/handler_test.go index 4f09c6be..eaface1e 100644 --- a/internal/command/handler_test.go +++ b/internal/command/handler_test.go @@ -29,10 +29,6 @@ type fakeSubagentService struct { items []subagent.Subagent } -func (f *fakeSubagentService) list() []subagent.Subagent { - return f.items -} - type fakeScheduleService struct { items []schedule.Schedule } @@ -325,7 +321,7 @@ func TestNewCommands_NilServices(t *testing.T) { } } -// suppress unused warnings +// suppress unused warnings. var ( _ = fakeSubagentService{items: []subagent.Subagent{{ID: "1", Name: "test", CreatedAt: time.Now(), UpdatedAt: time.Now()}}} _ = fakeScheduleService{items: []schedule.Schedule{{ID: "1", Name: "test"}}} diff --git a/internal/command/parser.go b/internal/command/parser.go index a9642077..7f043905 100644 --- a/internal/command/parser.go +++ b/internal/command/parser.go @@ -13,7 +13,7 @@ type ParsedCommand struct { } // Parse parses a raw command string into its components. -// Expected format: /resource [action] [args...] +// Expected format: /resource [action] [args...]. func Parse(text string) (ParsedCommand, error) { text = strings.TrimSpace(text) if !strings.HasPrefix(text, "/") { diff --git a/internal/command/schedule.go b/internal/command/schedule.go index 99a0a64c..e7d01fa6 100644 --- a/internal/command/schedule.go +++ b/internal/command/schedule.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "strconv" "strings" "github.com/memohai/memoh/internal/schedule" @@ -45,7 +46,7 @@ func (h *Handler) buildScheduleGroup() *CommandGroup { } maxCalls := "unlimited" if item.MaxCalls != nil { - maxCalls = fmt.Sprintf("%d", *item.MaxCalls) + maxCalls = strconv.Itoa(*item.MaxCalls) } return formatKV([]kv{ {"Name", item.Name}, @@ -54,7 +55,7 @@ func (h *Handler) buildScheduleGroup() *CommandGroup { {"Command", item.Command}, {"Enabled", boolStr(item.Enabled)}, {"Max Calls", maxCalls}, - {"Current Calls", fmt.Sprintf("%d", item.CurrentCalls)}, + {"Current Calls", strconv.Itoa(item.CurrentCalls)}, {"Created", item.CreatedAt.Format("2006-01-02 15:04:05")}, {"Updated", item.UpdatedAt.Format("2006-01-02 15:04:05")}, }), nil diff --git a/internal/command/settings.go b/internal/command/settings.go index 894cb4dc..3aab317c 100644 --- a/internal/command/settings.go +++ b/internal/command/settings.go @@ -23,8 +23,8 @@ func (h *Handler) buildSettingsGroup() *CommandGroup { {"Language", s.Language}, {"Allow Guest", boolStr(s.AllowGuest)}, {"Max Context Load Time", fmt.Sprintf("%d min", s.MaxContextLoadTime)}, - {"Max Context Tokens", fmt.Sprintf("%d", s.MaxContextTokens)}, - {"Max Inbox Items", fmt.Sprintf("%d", s.MaxInboxItems)}, + {"Max Context Tokens", strconv.Itoa(s.MaxContextTokens)}, + {"Max Inbox Items", strconv.Itoa(s.MaxInboxItems)}, {"Reasoning Enabled", boolStr(s.ReasoningEnabled)}, {"Reasoning Effort", s.ReasoningEffort}, {"Heartbeat Enabled", boolStr(s.HeartbeatEnabled)}, @@ -126,13 +126,6 @@ func settingsUpdateUsage() string { "- --heartbeat_model_id " } -func valueOrNone(s string) string { - if s == "" { - return "(none)" - } - return s -} - // resolveModelName resolves a model UUID to "model_name (provider_name)". func (h *Handler) resolveModelName(cc CommandContext, modelID string) string { if modelID == "" {