agentsclimarketplace

Emailops

Skill gerodp/hermes-productivity-skills/email/emailops

Productivity skills tap for the Hermes agent: weekly report, time-tracking, notes, email, calendar, git, and analytics.

Install
npx -y skills add gerodp/hermes-productivity-skills --skill emailops

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Query, search, chat with, and compose/draft/send email from a local EmailOps mailbox via emailops-cli.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

12.6 KB, as published. Nobody here has run it

EmailOps CLI

emailops-cli is the headless front-end for EmailOps, a privacy-first, AI-native email client. AI runs locally by default (embedded llama.cpp); email data lives in a local SQLite database. This skill lets the agent read, search, chat with, and compose / draft / send mail without opening the desktop app.

It complements the himalaya skill: Himalaya speaks raw IMAP/SMTP, while emailops-cli adds Gmail/Outlook OAuth accounts plus local AI — semantic chat over your mail, classification, and embeddings.

The agent contract: always pass --json

Every command supports --json, which prints exactly one stable envelope to stdout (logs always go to stderr). Parse this single shape regardless of success or failure — never scrape the human-readable output:

{ "ok": true,  "data": { /* result */ }, "error": null }                                  // success
{ "ok": false, "data": null, "error": { "code": "not_found", "params": {}, "message": "" } } // failure

Add --quiet to suppress the stderr app-log stream when you only want the envelope.

Exit codes (branch on these, not on text)

CodeMeaningCodeMeaning
0ok5network / sync
2invalid input6AI
3not found130cancelled
4auth1otherwise

Prerequisites

  1. emailops-cli on PATH (emailops-cli --version to verify). It is built from the EmailOps repo with the cli cargo feature; symlink the built binary into ~/.local/bin if needed.
  2. An EmailOps data directory with at least one configured account. Confirm with doctor before anything else.

When to use

Use whenever the user asks about their own email: searching messages, asking "what did X say about Y?", listing accounts, reading a specific message, or checking mailbox health. Also use it to write mail — save or edit a draft, review existing drafts, or send a message. Prefer the cheap read paths (doctor, accounts, emails, show, search) over chat unless the question genuinely needs the local LLM.

Core commands

CommandPurposeCost
emailops-cli doctor --jsonEnv readiness: DB, accounts, AI config. Loads no model.cheap
emailops-cli accounts --jsonList configured accounts (id, email, provider).cheap
emailops-cli emails --json [--limit N] [--mailbox inbox|sent|spam|trash]List recent emails.cheap
emailops-cli show <ID> --jsonFull single email (headers + body).cheap
emailops-cli search '<query>' --json [--limit N] [--trace]Full-text search across an account's mail.cheap
emailops-cli chat '<question>' --json [--trace]Ask the local AI a question against your mail.loads LLM
emailops-cli stats --jsonPer-account dashboard totals + coverage.cheap
emailops-cli compose --to … --subject … (--body …|--body-file …) --jsonSave a draft (default) or --send to deliver.cheap (write)
emailops-cli drafts --jsonList saved drafts, newest-first.cheap
emailops-cli draft <ID> --jsonShow a single draft (recipients, subject, body, attachments).cheap

Scope to an account with --account <ID|EMAIL> (defaults to the single enabled account). When multiple accounts exist, list them first and pass --account explicitly.

Procedure

  1. Health check first (read-only, no model load):

    emailops-cli doctor --json
    

    Confirm data.ok == true and data.accountsEnabled > 0. If aiEnabled is false, avoid chat and stick to search/read commands.

  2. Resolve the account when there's more than one:

    emailops-cli accounts --json
    

    Use the returned id or email with --account.

  3. Read paths for lookups (fast, safe even while the desktop app is open):

    emailops-cli search 'invoice from acme' --json --limit 5
    emailops-cli show <id> --json
    emailops-cli emails --json --mailbox inbox --limit 25
    
  4. AI path for natural-language questions (loads the local model):

    emailops-cli chat 'what did the landlord say about the lease renewal?' --json
    

    Add --trace to inspect routing, retrieval, tool calls, and the sources behind the answer under data.trace.

  5. Multi-turn: carry context across one-shot invocations by passing the conversationId from a previous chat --json result:

    emailops-cli chat 'and what date was that?' --json --conversation <conversationId>
    
  6. Parse the envelope, branch on ok / exit code, and answer the user from data. Surface error.message on failure.

Writing mail: compose, drafts, send

compose is the single write path. By default it saves a draft (and, on Gmail/Outlook accounts, also pushes it to the provider's Drafts folder — no extra flag). Add --send to deliver immediately instead. Both paths return the resulting record under data.

🚫 NEVER send without explicit user confirmation. Do not run compose … --send until you have shown the user the full email about to be sent — the sending account, every To/Cc recipient, the subject, the complete body, and any attachments — and the user has explicitly approved that exact message. No exceptions: not for "obvious" replies, not because the user said "send it" earlier about a different draft, not to save a round-trip. A vague or prior "go ahead" is not sufficient — re-confirm after showing the final content. When unsure whether you have approval, save a draft and ask — you never need permission to save a draft, and you always need it to send.

# Save a draft (default). --to / --cc / --attach are repeatable.
emailops-cli --account <ID|EMAIL> compose \
  --to [email protected] --to [email protected] --cc [email protected] \
  --subject "Q3 numbers" \
  --body "Hi — draft here." \
  --attach /abs/path/report.pdf --json

# Long/rich body: read it from a file instead of --body (mutually exclusive).
emailops-cli --account <ID|EMAIL> compose \
  --to [email protected] --subject "Notes" --body-file /abs/path/body.txt --json

# Update an existing draft in place rather than creating a new one.
emailops-cli --account <ID|EMAIL> compose --draft <DRAFT_ID> \
  --subject "Q3 numbers (rev)" --body "Updated." --to [email protected] --json

# Send now — ONLY after showing the full email and getting explicit approval
# (see "Mandatory send procedure" below). Delivers immediately; irreversible.
emailops-cli --account <ID|EMAIL> compose \
  --to [email protected] --subject "Q3 numbers" --body "Final." --send --json

Rules and shapes:

  • --to, --cc, --attach are repeatable; pass the flag once per value.
  • Body is --body XOR --body-file. Supplying both is invalid input (exit 2, no JSON envelope — it's rejected at the argument layer). --body-file reads the body verbatim from the path.
  • Use real line breaks — write bodies like a normal email. The CLI stores the body verbatim and does not interpret escape sequences, so a plain --body "Hi\n\nText" is stored as the literal characters \n and the recipient sees \n in the message. Format the body the way a person would: greeting on its own line, a blank line between paragraphs, and a blank line before the sign-off. Two reliable ways to pass actual newlines:
    • --body-file (preferred for anything multi-line): write the body to a file with real newlines, then reference it — printf 'Hi there,\n\nFirst paragraph.\n\nThanks,\nGero\n' > /tmp/body.txt then compose … --body-file /tmp/body.txt.
    • ANSI-C quoting for short inline bodies: --body $'Hi there,\n\nText' (the $'…' form makes the shell expand \n into real newlines; ordinary "…" double quotes do not).
  • Attachments are path references, resolved at compose/send time. Use absolute paths readable now; filename and mimeType are inferred. Each returned attachment is {id, draftId, filePath, filename, mimeType}.
  • compose (draft) returns the draft object: id, accountId, status ("draft"), subject, toAddresses[], ccAddresses[], body, attachments[], providerDraftId (null on IMAP/local; set once pushed to Gmail/Outlook), createdAt/updatedAt. drafts returns an array of these (newest-first); draft <ID> returns one.
  • Drafts don't require recipients (a bare draft with empty toAddresses is saved fine) — but confirm the recipient list before --send.

Review drafts:

emailops-cli --account <ID|EMAIL> drafts --json      # newest-first list
emailops-cli --account <ID|EMAIL> draft <DRAFT_ID> --json  # one draft, full

Mandatory send procedure (never skip a step)

--send is irreversible and there is no undo. Every send MUST follow this order:

  1. Save it as a draft first (compose …, no --send) — never build the send command straight from user text.
  2. Show the user the full outgoing email by reading it back with draft <ID> --json: the sending account, all To/Cc recipients, the subject, the entire body, and the filename of each attachment. Present it plainly, not just a summary.
  3. Ask for explicit approval of that exact message and wait for a clear "yes, send this". If the user asks for any change, edit the draft (compose --draft <ID> …) and repeat from step 2 — approval of an earlier version does not carry over.
  4. Only then run compose --draft <ID> --send --json (or an equivalent --send with the identical, already-approved fields).

If you cannot show the full message and get a fresh, explicit yes, do not send — leave the draft saved and tell the user it's ready to review.

Pitfalls

  • Heavy writes need the app closed. sync, classify, embed, and the heavy compose write paths — --send, and provider draft push on Gmail/Outlook — mutate the SQLite DB (and hit the network); run them only when the EmailOps desktop app is not running (WAL contention). A plain save-as-draft on an IMAP/local account is safe even while the app is open (the draft stays local — providerDraftId is null). Read commands are always safe.

  • Don't scrape pretty output. Human output is styled (color/tables) only on an interactive TTY and is not a stable contract. Always --json.

  • Logs are on stderr. Don't merge stderr into stdout when parsing JSON; use --quiet to silence the app-log stream.

  • No model for doctor. It's intentionally cheap — use it as the gate before any AI command rather than calling chat blindly.

  • Never send without explicit user confirmation. --send is irreversible; always follow the Mandatory send procedure above — save a draft, show the full email (account, all recipients, subject, body, attachments), get a fresh explicit yes, then send. When in doubt, leave it as a draft.

  • Same commands in the REPL. Running emailops-cli with no subcommand opens an interactive REPL that exposes these as /compose, /drafts, and /draft <id>. For the agent, prefer the one-shot subcommands with --json.

  • Double-encoded saved files. When emailops-cli output is written to a file and ends up double-escaped (the outer JSON has an "output" key whose value is a string containing the inner JSON), unwrap it first:

    cat saved_result.json | jq -r '.output' | jq ...
    

    Without -r .output, jq will try to parse the escaped string as literal JSON tokens and silently fail or return wrong results.

  • Outlook/Hotmail control characters break jq. Emails from Outlook/Hotmail providers can inject raw \r\n sequences that cause jq: parse error: Invalid string: control characters … errors. Pre-filter before piping to jq:

    cat output.json | sed 's/\\r//g' | jq ...
    

Verification

emailops-cli doctor --json   # expect ok:true, accountsEnabled > 0, exit 0
emailops-cli accounts --json # expect a non-empty data array

If doctor returns ok:true with at least one enabled account, the skill is correctly wired and downstream commands can be trusted.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.