feat(cli): folders command, search --all-folders, empty-search hint
Fixes from testing email search (docs/enhancements-2026-07-07.md): - New `folders` agent command lists the account's mailboxes (name, delimiter, selectable), INBOX first, so agents can discover archived mail outside INBOX. - `search --all-folders` sweeps every selectable mailbox; each hit carries a `folder` field, `skipped_folders` reports mailboxes the server refused, and --limit caps visible results across the sweep. The sweep deliberately skips EnsureFolderBaseline so a read-only search never mutates list --new state. - Empty search results include a generic `data.hint` with next steps. The hint is a fixed constant per mode, so the invisibility invariant holds: absent and policy-filtered mail produce byte-identical envelopes (codified in TestSearchEmptyHintIndistinguishableFromFiltered). - Skill and user docs: document `--text` full-text search as best-effort (server-dependent); recommend --subject-contains/--from. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+54
-1
@@ -165,6 +165,8 @@ func Run(args []string, out, errOut io.Writer) int {
|
||||
switch normalizeVerb(cmd) {
|
||||
case "list", "get", "search", "ack":
|
||||
return runAgent(normalizeVerb(cmd), rest, role, out, errOut)
|
||||
case "folders":
|
||||
return runFolders(rest, role, out, errOut)
|
||||
case "send":
|
||||
return runSend(rest, role, out, errOut)
|
||||
case "account":
|
||||
@@ -185,6 +187,37 @@ func Run(args []string, out, errOut io.Writer) int {
|
||||
}
|
||||
}
|
||||
|
||||
// runFolders handles `folders --account <name>` — list the account's mailboxes.
|
||||
func runFolders(args []string, role store.Role, out, errOut io.Writer) int {
|
||||
fs := flag.NewFlagSet("folders", flag.ContinueOnError)
|
||||
fs.SetOutput(errOut)
|
||||
usageFlags(fs, "folders", errOut)
|
||||
account := fs.String("account", "", "account name")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return 0
|
||||
}
|
||||
_ = Failure(CodeUsage, err.Error()).Write(out)
|
||||
return 2
|
||||
}
|
||||
if *account == "" {
|
||||
_ = Failure(CodeUsage, "--account is required").Write(out)
|
||||
return 2
|
||||
}
|
||||
st, err := openStore(role)
|
||||
if err != nil {
|
||||
_ = Failure(CodeConfig, err.Error()).Write(out)
|
||||
return 1
|
||||
}
|
||||
defer st.Close()
|
||||
_, _ = st.PurgeAudit(time.Now())
|
||||
d := newDepsLive(st, out)
|
||||
if err := FoldersCmd(d, *account); err != nil {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// runAgent handles JSON-emitting commands. Errors are emitted as JSON envelopes.
|
||||
func runAgent(cmd string, args []string, role store.Role, out, errOut io.Writer) int {
|
||||
fs := flag.NewFlagSet(cmd, flag.ContinueOnError)
|
||||
@@ -203,6 +236,10 @@ func runAgent(cmd string, args []string, role store.Role, out, errOut io.Writer)
|
||||
sinceDate := fs.String("since-date", "", "search: RFC3339 date lower bound")
|
||||
beforeDate := fs.String("before-date", "", "search: RFC3339 date upper bound")
|
||||
ackUIDs := fs.String("uid-list", "", "ack: comma-separated UIDs")
|
||||
allFolders := new(bool)
|
||||
if cmd == "search" {
|
||||
allFolders = fs.Bool("all-folders", false, "search: sweep all mailboxes")
|
||||
}
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return 0 // usage already printed to stderr; help isn't an error
|
||||
@@ -217,6 +254,18 @@ func runAgent(cmd string, args []string, role store.Role, out, errOut io.Writer)
|
||||
_ = Failure(CodeUsage, "--account is required").Write(out)
|
||||
return 2
|
||||
}
|
||||
if *allFolders {
|
||||
folderSet := false
|
||||
fs.Visit(func(f *flag.Flag) {
|
||||
if f.Name == "folder" {
|
||||
folderSet = true
|
||||
}
|
||||
})
|
||||
if folderSet {
|
||||
_ = Failure(CodeUsage, "--folder and --all-folders are mutually exclusive").Write(out)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
st, err := openStore(role)
|
||||
if err != nil {
|
||||
_ = Failure(CodeConfig, err.Error()).Write(out)
|
||||
@@ -251,7 +300,11 @@ func runAgent(cmd string, args []string, role store.Role, out, errOut io.Writer)
|
||||
sc.Before = tm
|
||||
}
|
||||
}
|
||||
if err := SearchCmd(d, *account, *folder, sc, *limit); err != nil {
|
||||
if *allFolders {
|
||||
if err := SearchAllCmd(d, *account, sc, *limit); err != nil {
|
||||
return 1
|
||||
}
|
||||
} else if err := SearchCmd(d, *account, *folder, sc, *limit); err != nil {
|
||||
return 1
|
||||
}
|
||||
case "ack":
|
||||
|
||||
Reference in New Issue
Block a user