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
17 changes: 17 additions & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ hey completion
hey compose
hey compose --bcc
hey compose --cc
hey compose --draft
hey compose --message
hey compose --subject
hey compose --thread-id
Expand All @@ -40,6 +41,21 @@ hey config
hey config set
hey config show
hey doctor
hey draft
hey draft create
hey draft create --bcc
hey draft create --cc
hey draft create --message
hey draft create --subject
hey draft create --thread-id
hey draft create --to
hey draft delete
hey draft update
hey draft update --bcc
hey draft update --cc
hey draft update --message
hey draft update --subject
hey draft update --to
hey drafts
hey drafts --all
hey drafts --limit
Expand All @@ -61,6 +77,7 @@ hey recordings --ends-on
hey recordings --limit
hey recordings --starts-on
hey reply
hey reply --draft
hey reply --message
hey seen
hey setup
Expand Down
30 changes: 30 additions & 0 deletions DRAFT_SUPPORT_NOTE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Draft Support Implementation Note

## Authorship

Authored by Mike Gyi with Codex 5.5.

## Intent

The goal of this work is to make HEY draft workflows scriptable from the CLI while keeping Basecamp's CLI taste as the reference point.

The public Basecamp CLI treats drafts as part of the normal creation flow, for example with a `--draft` option on a create command. This HEY CLI change follows that shape by adding draft mode to the existing mail commands:

```sh
hey compose --draft --to person@example.com --subject "Hello" -m "Draft body"
hey reply 123 --draft -m "Thanks!"
```

Explicit draft commands are still included because they are useful for agent and scripting workflows:

```sh
hey draft create --to person@example.com --subject "Hello" -m "Draft body"
hey draft update 123 --subject "Updated subject" -m "Updated body"
hey draft delete 123
```

## Implementation Constraint

The current HEY SDK surface does not expose first-class draft create/update/delete methods. The implementation therefore uses the authenticated HEY web form flow for draft mutations, with CSRF parsing and fail-closed form validation around the fields needed by the CLI.

If HEY later exposes official draft endpoints in the SDK, the CLI command surface should stay the same and only the internal draft implementation should move to the official API.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,14 @@ hey boxes # list mailboxes
hey box imbox # list postings in a box (by name or ID)
hey threads 123 # read a full email thread
hey reply 123 -m "Thanks!" # reply to a thread (or omit -m to open $EDITOR)
hey reply 123 --draft -m "Thanks!" # save a reply draft without sending
hey compose --to user@example.com --subject "Hello" # compose a new message
hey compose --draft --to user@example.com --subject "Hello" -m "Draft body" # save without sending
hey compose --to user@example.com --cc bob@example.com --bcc carol@example.org --subject "Hello" # with CC/BCC
hey draft create --to user@example.com --subject "Hello" -m "Draft body" # save without sending
hey draft create --thread-id 123 -m "Thanks!" # save a reply draft without sending
hey draft update 456 --to user@example.com --subject "Hello" -m "Updated body"
hey draft delete 456
hey drafts # list drafts
```

Expand Down
20 changes: 19 additions & 1 deletion 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
draft bool
}

func newComposeCommand() *composeCommand {
Expand All @@ -27,9 +28,10 @@ func newComposeCommand() *composeCommand {
Use: "compose",
Short: "Compose a new message",
Annotations: map[string]string{
"agent_notes": "Creates a new email. Requires --subject. Use --to (optionally with --cc/--bcc) for new threads or --thread-id for existing ones.",
"agent_notes": "Creates and sends a new email unless --draft is set. Requires --subject. Use --to (optionally with --cc/--bcc) for new threads or --thread-id for existing ones.",
},
Example: ` hey compose --to alice@example.com --subject "Hello" -m "Hi there"
hey compose --draft --to alice@example.com --subject "Hello" -m "Draft body"
hey compose --to alice@example.com --cc bob@example.com --bcc carol@example.org --subject "Hello" -m "Hi"
hey compose --subject "Update" --thread-id 12345 -m "Thread reply"
echo "Long message" | hey compose --to bob@example.com --subject "Report"`,
Expand All @@ -42,6 +44,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().BoolVar(&composeCommand.draft, "draft", false, "Save as draft instead of sending")

return composeCommand
}
Expand Down Expand Up @@ -85,13 +88,28 @@ func (c *composeCommand) run(cmd *cobra.Command, args []string) error {
if err != nil {
return output.ErrUsage(fmt.Sprintf("invalid thread ID: %s", c.threadID))
}
if c.draft {
return createReplyDraft(ctx, cmd.OutOrStdout(), topicID, draftFormRequest{
Subject: c.subject,
Content: message,
})
}
Comment on lines +91 to +96
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 c.draft {
return createMessageDraft(ctx, cmd.OutOrStdout(), draftFormRequest{
Subject: c.subject,
Content: message,
To: to,
CC: cc,
BCC: bcc,
})
}
Comment on lines +104 to +112
if err := sdk.Messages().Create(ctx, c.subject, message, to, cc, bcc); err != nil {
return convertSDKError(err)
}
Expand Down
Loading