Skip to content

feat: add --from flag and default-sender, add screening new mail control from cli#73

Open
erikd234 wants to merge 6 commits intobasecamp:mainfrom
luna-labs-hq:main
Open

feat: add --from flag and default-sender, add screening new mail control from cli#73
erikd234 wants to merge 6 commits intobasecamp:mainfrom
luna-labs-hq:main

Conversation

@erikd234
Copy link
Copy Markdown

@erikd234 erikd234 commented Apr 12, 2026

  • Add --from flag to compose and reply commands to override sender
  • Add default_sender config (hey config set/get/unset default-sender)
  • Priority: --from flag > config default_sender > HEY default
  • Resolves sender email to ID via identity endpoint
  • Bypasses SDK service methods when override is active to control acting_sender_id

Summary by cubic

Add sender override to compose and reply, plus a new hey screener command to manage The Screener. This lets you choose the sender per message and screen senders into Imbox, Feed, or Paper Trail from the CLI.

  • New Features

    • --from on hey compose and hey reply to set the sender email per message.
    • New config key default_sender; manage with hey config set|get|unset default-sender (hyphen or underscore accepted).
    • Priority: --from > default_sender > HEY default. Sender email is resolved to an ID via the identity endpoint.
    • When overriding, requests send acting_sender_id via raw mutations; otherwise behavior is unchanged.
    • hey config show now includes default_sender.
    • New hey screener to list pending items and approve|deny|spam|feed|trail <posting-id>; uses /clearances HTML and PATCH /clearances/<id> while SDK support is pending.
  • Bug Fixes

    • Correctly parse Feed/Paper Trail box IDs in hey screener (multiline regex).
    • Removed all personal emails; hey screener now filters your own addresses via identity lookup.
    • Added tests for hey screener (list/actions) and config key normalization.

Written for commit 1b43316. Summary will update on new commits.

- Add --from flag to compose and reply commands to override sender
- Add default_sender config (hey config set/get/unset default-sender)
- Priority: --from flag > config default_sender > HEY default
- Resolves sender email to ID via identity endpoint
- Bypasses SDK service methods when override is active to control acting_sender_id
Copilot AI review requested due to automatic review settings April 12, 2026 00:36
…pdate

- Config tests: default value, global config load, set/unset, save/reload,
  JSON omitempty, Values() output
- Cmd tests: normalizeConfigKey hyphen-to-underscore conversion
- Update surface snapshot for new --from flags and config get/unset commands
@github-actions github-actions bot added the enhancement New feature or request label Apr 12, 2026
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 5 files

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds support for overriding the sender identity for compose/reply via a new --from flag and a persisted default_sender config value.

Changes:

  • Introduces default_sender configuration (load/save/show, set/get/unset) with source tracking.
  • Adds sender resolution via the identity endpoint and uses acting_sender_id when an override is active.
  • Extends CLI surface/options for compose and reply with --from.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
internal/config/config_default_sender_test.go Adds unit tests covering default_sender behavior and persistence.
internal/config/config.go Adds DefaultSender field, load/save behavior, values listing, and unset support.
internal/cmd/sender_test.go Tests CLI config-key normalization (hyphen → underscore).
internal/cmd/sender.go Implements sender ID resolution and sender selection priority.
internal/cmd/reply.go Adds --from and bypasses SDK service call to set acting_sender_id when needed.
internal/cmd/config.go Adds config get/unset, supports default_sender, improves key normalization and errors.
internal/cmd/compose.go Adds --from and bypasses SDK service call to set acting_sender_id when needed.
.surface Updates surfaced commands/flags for completions/docs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +138 to +140
"entry": map[string]any{
"addressed": addressed,
},
Comment on lines +31 to +34
return 0, output.ErrUsage(fmt.Sprintf(
"no sender matching %q (available: %s)",
email, strings.Join(available, ", "),
))
Comment on lines 212 to +223
}
return nil
}

// UnsetField clears a configuration field so it reverts to default.
func (c *Config) UnsetField(key string) {
switch key {
case "default_sender":
c.DefaultSender = ""
if c.sources != nil {
c.sources["default_sender"] = SourceDefault
}
Comment on lines +95 to +106
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,
},
}
Adds 'hey screener' with subcommands:
- list: show pending screener items
- approve <id>: screen in to Imbox
- deny <id>: screen out
- spam <id>: mark as spam
- feed <id>: screen in to The Feed
- trail <id>: screen in to Paper Trail

Uses the /clearances HTML endpoint for listing and
PATCH /clearances/<id> for actions, since the SDK
doesn't have a Clearances service yet.
- 12 tests covering list (empty/populated), approve, deny, spam,
  feed, trail, and missing-args validation for all subcommands
- httptest server mocks the /clearances HTML and PATCH endpoints
- Fixed feed/trail box ID regex to use (?s) for cross-line matching
- Updated surface snapshot for new screener commands
Copilot AI review requested due to automatic review settings April 12, 2026 16:40
@luna-parrot
Copy link
Copy Markdown

🔍 New: hey screener command

Added a screener command to manage The Screener (clearances) from the CLI.

Commands

hey screener              # List pending screener items
hey screener approve <id> # Screen in to Imbox
hey screener deny <id>    # Screen out
hey screener spam <id>    # Mark as spam
hey screener feed <id>    # Screen in to The Feed
hey screener trail <id>   # Screen in to Paper Trail

How it works

Since the SDK doesn't have a Clearances service yet, this uses the web API directly:

  • Listing: GET /clearances (HTML) — parses posting IDs, sender emails, and subjects from the page
  • Actions: PATCH /clearances/<id> with form params:
    • status=approved → Imbox
    • status=approved&designation_box_id=<feed_id> → Feed
    • status=approved&designation_box_id=<trail_id> → Paper Trail
    • status=denied → Screen out
    • status=denied&spam=true → Spam

Feed/Trail box IDs are dynamically extracted from the HTML hidden inputs, so they're account-specific.

Tests

12 tests in screener_test.go covering:

  • List (empty + populated)
  • All 5 actions (approve, deny, spam, feed, trail)
  • Missing-args validation for all subcommands
  • Uses httptest.Server mocking the /clearances HTML + PATCH endpoints
  • Surface snapshot updated

Files changed

  • internal/cmd/screener.go — command implementation
  • internal/cmd/screener_test.go — tests
  • internal/cmd/root.go — command registration
  • .surface — snapshot update

Once the SDK adds a Clearances service, the HTTP calls can be swapped out for SDK methods.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds sender override support for hey compose/hey reply via a new --from flag and persisted default_sender config, and introduces a new hey screener command to manage pending sender approvals from the CLI.

Changes:

  • Add default_sender to config (load/save/show, set/get/unset) with hyphen/underscore key normalization.
  • Add --from to compose/reply and resolve sender email to acting_sender_id via identity endpoint, bypassing SDK helpers when overridden.
  • Add hey screener command (plus tests) to list and action pending clearances using HTML scraping + PATCH.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
internal/config/config.go Adds DefaultSender field, sources tracking, flag setter, unset helper, and includes value in Values().
internal/config/config_default_sender_test.go Adds tests for default_sender defaults, persistence, sources, and JSON omission when empty.
internal/cmd/config.go Adds config key normalization + get/unset subcommands and includes default_sender in show.
internal/cmd/compose.go Adds --from and override path that posts raw mutations with acting_sender_id.
internal/cmd/reply.go Adds --from and override path that posts raw mutations with acting_sender_id.
internal/cmd/sender.go Introduces sender email → sender ID resolution and “effective sender” selection logic.
internal/cmd/sender_test.go Tests config key normalization behavior.
internal/cmd/screener.go Introduces hey screener command, HTML scraping for items/box IDs, and PATCH actions.
internal/cmd/screener_test.go Adds integration-style tests with an HTTP test server for list/actions and missing-arg errors.
internal/cmd/root.go Registers the new screener command.
.surface Updates CLI surface list with new flags/subcommands (from/get/unset/screener).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +212 to +222
// Extract emails
emailRe := regexp.MustCompile(`[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}`)
allEmails := emailRe.FindAllString(body, -1)

// Filter out the user's own emails (they appear in nav/header)
senderEmails := []string{}
for _, e := range allEmails {
lower := strings.ToLower(e)
if lower != "erik.dahl@hey.com" && lower != "erik@parrotapp.com" {
senderEmails = append(senderEmails, e)
}
Comment on lines +212 to +214
// Extract emails
emailRe := regexp.MustCompile(`[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}`)
allEmails := emailRe.FindAllString(body, -1)
Comment on lines +225 to +227
// Extract subjects
subjectRe := regexp.MustCompile(`<span[^>]*>([^<]*(?:Re:|Fwd:|We'|Your |Hi |Hello|Thank|Welcome|Confirm|Order|Invoice|Receipt|Upgrade)[^<]*)</span>`)
subjects := subjectRe.FindAllStringSubmatch(body, -1)
Comment on lines +262 to +272
// Build items: match posting IDs with senders and subjects
for i, id := range uniqueIDs {
item := screenerItem{PostingID: id}
if i < len(senderEmails) {
item.Sender = senderEmails[i]
}
if i < len(subjectTexts) {
item.Subject = subjectTexts[i]
}
items = append(items, item)
}
// Extract feed/trail box IDs from hidden form inputs.
// The HTML has forms where designation_box_id appears before the feedbox/trailbox button,
// so we use (?s) to match across newlines.
feedRe := regexp.MustCompile(`(?s)designation_box_id[^>]*value="(\d+)"[^<]*</input>?[^<]*<button[^>]*feedboxButton`)
}
}

trailRe := regexp.MustCompile(`(?s)designation_box_id[^>]*value="(\d+)"[^<]*</input>?[^<]*<button[^>]*trailboxButton`)
Comment on lines +31 to +34
return 0, output.ErrUsage(fmt.Sprintf(
"no sender matching %q (available: %s)",
email, strings.Join(available, ", "),
))
return &cobra.Command{
Use: "unset <key>",
Short: "Clear a configuration value",
Example: ` hey config unset default_sender`,
@erikd234 erikd234 changed the title feat: add --from flag and default-sender config for compose/reply feat: add --from flag and default-sender, add screening new mail control from cli Apr 12, 2026
- Replace erik@parrotapp.com in test with generic alice@hey.com
- Replace hardcoded email filter in screener with dynamic identity lookup
- No personal emails remain in source code
Copilot AI review requested due to automatic review settings April 12, 2026 17:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds sender selection controls and screener management to the HEY CLI, enabling per-message sender overrides and basic Screener workflows from the command line.

Changes:

  • Add --from sender override to hey compose and hey reply, plus default_sender config support with precedence rules.
  • Extend hey config with get/unset and key normalization (default-senderdefault_sender).
  • Introduce hey screener command (list + approve/deny/spam/feed/trail) with HTML parsing and PATCH actions, plus tests.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
internal/config/config.go Adds DefaultSender field, source tracking, and unset support.
internal/config/config_default_sender_test.go Tests default-sender behavior, persistence, and JSON omission.
internal/cmd/config.go Adds key normalization plus config get/unset; updates show.
internal/cmd/compose.go Implements sender override behavior via raw mutations when active.
internal/cmd/reply.go Implements sender override behavior via raw mutations when active.
internal/cmd/sender.go Adds sender ID resolution and effective sender selection logic.
internal/cmd/sender_test.go Tests config key normalization.
internal/cmd/screener.go Adds screener command, HTML parsing, and clearance PATCH actions.
internal/cmd/screener_test.go Adds screener list/action tests using an HTTP test server.
internal/cmd/root.go Registers the new screener command.
.surface Updates CLI surface list to include new flags/commands.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +138 to +140
"entry": map[string]any{
"addressed": addressed,
},
Comment on lines +84 to +89
items, feedBoxID, trailBoxID, err := fetchScreenerItems(cmd.Context())
if err != nil {
return err
}
_ = feedBoxID
_ = trailBoxID
Comment on lines +245 to +266
feedRe := regexp.MustCompile(`(?s)designation_box_id[^>]*value="(\d+)"[^<]*</input>?[^<]*<button[^>]*feedboxButton`)
if m := feedRe.FindStringSubmatch(body); m != nil {
feedBoxID = m[1]
}
// Alternative pattern: button target appears after the input in the same form
if feedBoxID == "" {
altFeedRe := regexp.MustCompile(`(?s)designation_box_id"[^>]*value="(\d+)".*?feedboxButton`)
if m := altFeedRe.FindStringSubmatch(body); m != nil {
feedBoxID = m[1]
}
}

trailRe := regexp.MustCompile(`(?s)designation_box_id[^>]*value="(\d+)"[^<]*</input>?[^<]*<button[^>]*trailboxButton`)
if m := trailRe.FindStringSubmatch(body); m != nil {
trailBoxID = m[1]
}
if trailBoxID == "" {
altTrailRe := regexp.MustCompile(`(?s)designation_box_id"[^>]*value="(\d+)".*?trailboxButton`)
if m := altTrailRe.FindStringSubmatch(body); m != nil {
trailBoxID = m[1]
}
}
Comment on lines +299 to +303
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // Don't follow redirects
},
}
return "", err
}

client := &http.Client{}
Comment on lines +288 to +289
reqURL := cfg.BaseURL + "/clearances/" + postingID
req, err := http.NewRequestWithContext(ctx, "PATCH", reqURL, strings.NewReader(values.Encode()))
Action: "deny",
Command: "hey screener deny <id>",
Description: "Screen out sender",
},
fmt.Fprintf(w, clearancesHTMLTemplate, itemsHTML)

case r.Method == "PATCH" && strings.HasPrefix(r.URL.Path, "/clearances/"):
_ = r.ParseForm()
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/cmd/screener.go">

<violation number="1" location="internal/cmd/screener.go:219">
P2: Identity lookup errors are silently ignored, which can leave unfiltered self-emails and cause misleading sender-to-posting mapping in screener output.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

// Filter out the authenticated user's own emails (they appear in nav/header).
// Look up dynamically via the identity endpoint.
ownEmails := map[string]bool{}
if identity, err := sdk.Identity().GetIdentity(ctx); err == nil && identity != nil {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Identity lookup errors are silently ignored, which can leave unfiltered self-emails and cause misleading sender-to-posting mapping in screener output.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/cmd/screener.go, line 219:

<comment>Identity lookup errors are silently ignored, which can leave unfiltered self-emails and cause misleading sender-to-posting mapping in screener output.</comment>

<file context>
@@ -213,11 +213,17 @@ func fetchScreenerItems(ctx context.Context) ([]screenerItem, string, string, er
+	// Filter out the authenticated user's own emails (they appear in nav/header).
+	// Look up dynamically via the identity endpoint.
+	ownEmails := map[string]bool{}
+	if identity, err := sdk.Identity().GetIdentity(ctx); err == nil && identity != nil {
+		for _, s := range identity.Senders {
+			ownEmails[strings.ToLower(s.EmailAddress)] = true
</file context>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants