Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,16 @@ hey completion
hey compose
hey compose --bcc
hey compose --cc
hey compose --from
hey compose --message
hey compose --subject
hey compose --thread-id
hey compose --to
hey config
hey config get
hey config set
hey config show
hey config unset
hey doctor
hey drafts
hey drafts --all
Expand All @@ -61,7 +64,14 @@ hey recordings --ends-on
hey recordings --limit
hey recordings --starts-on
hey reply
hey reply --from
hey reply --message
hey screener
hey screener approve
hey screener deny
hey screener feed
hey screener spam
hey screener trail
hey seen
hey setup
hey skill
Expand Down
60 changes: 56 additions & 4 deletions internal/cmd/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type composeCommand struct {
subject string
message string
threadID string
from string
}

func newComposeCommand() *composeCommand {
Expand All @@ -42,6 +43,7 @@ func newComposeCommand() *composeCommand {
composeCommand.cmd.Flags().StringVar(&composeCommand.subject, "subject", "", "Message subject (required)")
composeCommand.cmd.Flags().StringVarP(&composeCommand.message, "message", "m", "", "Message body (or opens $EDITOR)")
composeCommand.cmd.Flags().StringVar(&composeCommand.threadID, "thread-id", "", "Thread ID to post message to")
composeCommand.cmd.Flags().StringVar(&composeCommand.from, "from", "", "Sender email address (overrides default)")

return composeCommand
}
Expand Down Expand Up @@ -80,20 +82,70 @@ func (c *composeCommand) run(cmd *cobra.Command, args []string) error {

ctx := cmd.Context()

// When --from or default_sender is set, we bypass the SDK's service methods
// and call PostMutation directly so we can control the acting_sender_id.
hasSenderOverride := c.from != "" || cfg.DefaultSender != ""

if c.threadID != "" {
topicID, err := strconv.ParseInt(c.threadID, 10, 64)
if err != nil {
return output.ErrUsage(fmt.Sprintf("invalid thread ID: %s", c.threadID))
}
if err := sdk.Messages().CreateTopicMessage(ctx, topicID, message); err != nil {
return convertSDKError(err)
if hasSenderOverride {
senderID, err := effectiveSenderID(ctx, c.from)
if err != nil {
return err
}
body := map[string]any{
"acting_sender_id": senderID,
"message": map[string]any{
"content": message,
},
}
if _, err := sdk.PostMutation(ctx, fmt.Sprintf("/topics/%d/entries.json", topicID), body); err != nil {
return convertSDKError(err)
}
} else {
if err := sdk.Messages().CreateTopicMessage(ctx, topicID, message); err != nil {
return convertSDKError(err)
}
}
} else {
to := parseAddresses(c.to)
cc := parseAddresses(c.cc)
bcc := parseAddresses(c.bcc)
if err := sdk.Messages().Create(ctx, c.subject, message, to, cc, bcc); err != nil {
return convertSDKError(err)
if hasSenderOverride {
senderID, err := effectiveSenderID(ctx, c.from)
if err != nil {
return err
}
addressed := map[string]any{}
if len(to) > 0 {
addressed["directly"] = to
}
if len(cc) > 0 {
addressed["copied"] = cc
}
if len(bcc) > 0 {
addressed["blindcopied"] = bcc
}
body := map[string]any{
"acting_sender_id": senderID,
"message": map[string]any{
"subject": c.subject,
"content": message,
},
"entry": map[string]any{
"addressed": addressed,
},
Comment on lines +138 to +140
Comment on lines +138 to +140
}
if _, err := sdk.PostMutation(ctx, "/messages.json", body); err != nil {
return convertSDKError(err)
}
} else {
if err := sdk.Messages().Create(ctx, c.subject, message, to, cc, bcc); err != nil {
return convertSDKError(err)
}
}
}

Expand Down
90 changes: 86 additions & 4 deletions internal/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"strings"

"github.com/spf13/cobra"

Expand All @@ -12,6 +13,8 @@ type configCommand struct {
cmd *cobra.Command
}

var configKeys = []string{"base_url", "default_sender"}

func newConfigCommand() *configCommand {
configCommand := &configCommand{}
configCommand.cmd = &cobra.Command{
Expand All @@ -21,27 +24,35 @@ func newConfigCommand() *configCommand {

configCommand.cmd.AddCommand(newConfigShowCommand())
configCommand.cmd.AddCommand(newConfigSetCommand())
configCommand.cmd.AddCommand(newConfigGetCommand())
configCommand.cmd.AddCommand(newConfigUnsetCommand())

return configCommand
}

// normalizeConfigKey converts hyphens to underscores for config key lookup.
func normalizeConfigKey(key string) string {
return strings.ReplaceAll(key, "-", "_")
}

func newConfigSetCommand() *cobra.Command {
return &cobra.Command{
Use: "set <key> <value>",
Short: "Set a configuration value in the global config",
Example: ` hey config set base_url http://app.hey.localhost:3003
hey config set base_url https://app.hey.com`,
hey config set base_url https://app.hey.com
hey config set default_sender you@hey.com`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
key, value := args[0], args[1]
key, value := normalizeConfigKey(args[0]), args[1]

switch key {
case "base_url":
case "base_url", "default_sender":
if err := cfg.SetFromFlag(key, value); err != nil {
return err
}
default:
return output.ErrUsage(fmt.Sprintf("unknown config key: %s (available: base_url)", key))
return output.ErrUsage(fmt.Sprintf("unknown config key: %s (available: %s)", key, strings.Join(configKeys, ", ")))
}

if err := cfg.Save(); err != nil {
Expand All @@ -59,6 +70,72 @@ func newConfigSetCommand() *cobra.Command {
}
}

func newConfigGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <key>",
Short: "Get a configuration value",
Example: ` hey config get default_sender
hey config get base_url`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
key := normalizeConfigKey(args[0])

var value string
switch key {
case "base_url":
value = cfg.BaseURL
case "default_sender":
value = cfg.DefaultSender
default:
return output.ErrUsage(fmt.Sprintf("unknown config key: %s (available: %s)", key, strings.Join(configKeys, ", ")))
}

if writer.IsStyled() {
if value == "" {
fmt.Fprintf(cmd.OutOrStdout(), "%s is not set\n", key)
} else {
fmt.Fprintln(cmd.OutOrStdout(), value)
}
return nil
}
return writeOK(map[string]string{"key": key, "value": value},
output.WithSummary(value),
)
},
}
}

func newConfigUnsetCommand() *cobra.Command {
return &cobra.Command{
Use: "unset <key>",
Short: "Clear a configuration value",
Example: ` hey config unset default_sender`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
key := normalizeConfigKey(args[0])

switch key {
case "default_sender":
cfg.UnsetField(key)
default:
return output.ErrUsage(fmt.Sprintf("cannot unset key: %s (unsettable keys: default_sender)", key))
}

if err := cfg.Save(); err != nil {
return err
}

if writer.IsStyled() {
fmt.Fprintf(cmd.OutOrStdout(), "Unset %s\n", key)
return nil
}
return writeOK(map[string]string{"key": key},
output.WithSummary(fmt.Sprintf("Unset %s", key)),
)
},
}
}

func newConfigShowCommand() *cobra.Command {
return &cobra.Command{
Use: "show",
Expand All @@ -70,6 +147,11 @@ func newConfigShowCommand() *cobra.Command {
"value": cfg.BaseURL,
"source": string(cfg.SourceOf("base_url")),
},
{
"key": "default_sender",
"value": cfg.DefaultSender,
"source": string(cfg.SourceOf("default_sender")),
},
}

if writer.IsStyled() {
Expand Down
38 changes: 36 additions & 2 deletions internal/cmd/reply.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
type replyCommand struct {
cmd *cobra.Command
message string
from string
}

func newReplyCommand() *replyCommand {
Expand All @@ -31,6 +32,7 @@ func newReplyCommand() *replyCommand {
}

replyCommand.cmd.Flags().StringVarP(&replyCommand.message, "message", "m", "", "Reply message (or opens $EDITOR)")
replyCommand.cmd.Flags().StringVar(&replyCommand.from, "from", "", "Sender email address (overrides default)")

return replyCommand
}
Expand Down Expand Up @@ -90,8 +92,40 @@ func (c *replyCommand) run(cmd *cobra.Command, args []string) error {
}
}

if err = sdk.Entries().CreateReply(ctx, latestEntryID, message, addressed.To, addressed.CC, addressed.BCC); err != nil {
return convertSDKError(err)
hasSenderOverride := c.from != "" || cfg.DefaultSender != ""
if hasSenderOverride {
senderID, err := effectiveSenderID(ctx, c.from)
if err != nil {
return err
}
body := map[string]any{
"acting_sender_id": senderID,
"message": map[string]any{
"content": message,
},
}
Comment on lines +95 to +106
addrMap := map[string]any{}
if len(addressed.To) > 0 {
addrMap["directly"] = addressed.To
}
if len(addressed.CC) > 0 {
addrMap["copied"] = addressed.CC
}
if len(addressed.BCC) > 0 {
addrMap["blindcopied"] = addressed.BCC
}
if len(addrMap) > 0 {
body["entry"] = map[string]any{
"addressed": addrMap,
}
}
if _, err := sdk.PostMutation(ctx, fmt.Sprintf("/entries/%d/replies.json", latestEntryID), body); err != nil {
return convertSDKError(err)
}
} else {
if err = sdk.Entries().CreateReply(ctx, latestEntryID, message, addressed.To, addressed.CC, addressed.BCC); err != nil {
return convertSDKError(err)
}
}

if writer.IsStyled() {
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ func newRootCmd() *cobra.Command {
root.AddCommand(newCompletionCommand())
root.AddCommand(newDoctorCommand())
root.AddCommand(newConfigCommand().cmd)
root.AddCommand(newScreenerCommand().cmd)

return root
}
Expand Down
Loading
Loading