Emailops
Productivity skills tap for the Hermes agent: weekly report, time-tracking, notes, email, calendar, git, and analytics.
npx -y skills add gerodp/hermes-productivity-skills --skill emailopsAssembled 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)
| Code | Meaning | Code | Meaning |
|---|---|---|---|
| 0 | ok | 5 | network / sync |
| 2 | invalid input | 6 | AI |
| 3 | not found | 130 | cancelled |
| 4 | auth | 1 | otherwise |
Prerequisites
emailops-clionPATH(emailops-cli --versionto verify). It is built from the EmailOps repo with theclicargo feature; symlink the built binary into~/.local/binif needed.- An EmailOps data directory with at least one configured account. Confirm
with
doctorbefore 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
| Command | Purpose | Cost |
|---|---|---|
emailops-cli doctor --json | Env readiness: DB, accounts, AI config. Loads no model. | cheap |
emailops-cli accounts --json | List 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> --json | Full 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 --json | Per-account dashboard totals + coverage. | cheap |
emailops-cli compose --to … --subject … (--body …|--body-file …) --json | Save a draft (default) or --send to deliver. | cheap (write) |
emailops-cli drafts --json | List saved drafts, newest-first. | cheap |
emailops-cli draft <ID> --json | Show 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
-
Health check first (read-only, no model load):
emailops-cli doctor --jsonConfirm
data.ok == trueanddata.accountsEnabled > 0. IfaiEnabledis false, avoidchatand stick to search/read commands. -
Resolve the account when there's more than one:
emailops-cli accounts --jsonUse the returned
idoremailwith--account. -
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 -
AI path for natural-language questions (loads the local model):
emailops-cli chat 'what did the landlord say about the lease renewal?' --jsonAdd
--traceto inspect routing, retrieval, tool calls, and the sources behind the answer underdata.trace. -
Multi-turn: carry context across one-shot invocations by passing the
conversationIdfrom a previouschat --jsonresult:emailops-cli chat 'and what date was that?' --json --conversation <conversationId> -
Parse the envelope, branch on
ok/ exit code, and answer the user fromdata. Surfaceerror.messageon 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 … --senduntil 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,--attachare repeatable; pass the flag once per value.- Body is
--bodyXOR--body-file. Supplying both is invalid input (exit 2, no JSON envelope — it's rejected at the argument layer).--body-filereads 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\nand the recipient sees\nin 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.txtthencompose … --body-file /tmp/body.txt.- ANSI-C quoting for short inline bodies:
--body $'Hi there,\n\nText'(the$'…'form makes the shell expand\ninto real newlines; ordinary"…"double quotes do not).
- Attachments are path references, resolved at compose/send time. Use
absolute paths readable now;
filenameandmimeTypeare 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.draftsreturns an array of these (newest-first);draft <ID>returns one.- Drafts don't require recipients (a bare draft with empty
toAddressesis 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:
- Save it as a draft first (
compose …, no--send) — never build the send command straight from user text. - 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. - 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. - Only then run
compose --draft <ID> --send --json(or an equivalent--sendwith 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 heavycomposewrite 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 —providerDraftIdis 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
--quietto silence the app-log stream. -
No model for
doctor. It's intentionally cheap — use it as the gate before any AI command rather than callingchatblindly. -
Never send without explicit user confirmation.
--sendis 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-cliwith 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,jqwill 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\nsequences that causejq: 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.