feat(skill): add emcli Agent Skill (agentskills.io standard)

skills/emcli/ — an Agent Skill teaching an agent to read and send mail through
emcli's JSON agent commands:

- SKILL.md: name/description (what + when + trigger keywords), compatibility,
  metadata; body covers the security model (agent-only commands, never touch
  EMCLI_KEY), setup, the list→get→ack workflow, sending, and enforcement
  awareness. Frontmatter validated against the spec (name matches dir; desc
  574/1024; compatibility 239/500); body 146 lines (<500).
- scripts/install.sh: detects OS/arch, downloads the release binary, verifies
  the sha256 checksum when present, fails gracefully. Release tag/assets
  (v0.4.0, emcli_<ver>_<os>_<arch>) are placeholders until the first release.
- references/{commands.md,install.md}: full agent command reference (flags, JSON
  shapes, error codes, enforcement) and install options, loaded on demand.

README links to the skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 20:25:08 +01:00
parent 55c763d641
commit 7ad4f1adc2
5 changed files with 475 additions and 0 deletions
+4
View File
@@ -18,3 +18,7 @@ emcli doctor # confirm it connects
See the **[User Manual](USER-MANUAL.md)** for full setup, account configuration (including Gmail See the **[User Manual](USER-MANUAL.md)** for full setup, account configuration (including Gmail
app passwords), the agent and admin command reference, the JSON output format, and troubleshooting. app passwords), the agent and admin command reference, the JSON output format, and troubleshooting.
For AI agents, **[`skills/emcli`](skills/emcli/SKILL.md)** is an
[Agent Skill](https://agentskills.io) that teaches an agent to read and send mail through `emcli`,
including a binary installer.
+146
View File
@@ -0,0 +1,146 @@
---
name: emcli
description: Read and send email through emcli, a guard-railed CLI gateway that mediates IMAP/SMTP so the agent never holds mail credentials. Use when a task involves checking or reading an inbox, listing or searching messages, fetching a message or its attachments, marking mail as processed, or sending or replying to email on the user's behalf. Works with Gmail and any IMAP/SMTP account; every command prints a single JSON object. Triggers include: check my email, read the inbox, list/search messages, get or download a message, reply to that email, send an email, process new mail.
compatibility: Requires the emcli binary (run scripts/install.sh to fetch it; needs curl or wget and a Linux/macOS/Windows shell) and the EMCLI_KEY environment variable, which the orchestrator provides. Needs network access to the configured mail server.
metadata:
author: steve
version: "0.4.0"
homepage: "https://gitea.dcglab.co.uk/steve/emcli"
---
# emcli — email for agents
`emcli` is a command-line gateway for email. You (the agent) call its **agent commands** to read
and send mail; the program holds the credentials and enforces the user's rules, so you never see a
password and cannot bypass the limits. Every agent command prints exactly **one line of JSON** and
sets its exit code to match.
## Security model — read this first
- **You only run agent commands:** `list`, `get`, `search`, `ack`, `send`. Account setup,
passwords, whitelists, and config are the **user's** job (admin commands) — do not run or suggest
running `account`, `whitelist`, `config`, `init`, or `doctor` unless the user explicitly asks you
to help administer.
- **Never touch the secret key.** `EMCLI_KEY` is supplied in the environment by whoever launched
you. Do not read it, print it, log it, pass it as an argument, or try to generate one. If it is
missing, stop and tell the user (see Setup).
- **Some mail is intentionally invisible.** The user may restrict which senders you can see and who
you can email. Blocked or filtered results are normal — handle them, don't try to work around
them (see Enforcement).
## Setup (do this once per session, before the first command)
1. **Check the binary is available.** Run `emcli version`. If the command is not found, install it:
```bash
bash scripts/install.sh
```
This downloads the binary from the project's releases and puts it on your PATH
(`~/.local/bin` by default). See [references/install.md](references/install.md) for options.
2. **Check the key is present.** Confirm the `EMCLI_KEY` environment variable is set (e.g.
`test -n "$EMCLI_KEY"`). **Do not print its value.** If it is empty, do not proceed — tell the
user: "emcli needs the EMCLI_KEY environment variable set by your orchestrator; I can't read or
create it for you."
3. **Find out which account(s) exist.** Ask the user for the account name (e.g. `gmail`, `work`),
or, if permitted, run `emcli doctor` once to see configured accounts and that they connect.
## How to read every result
Each command prints one JSON object:
```json
{ "error": false, "error_detail": {}, "data": { } }
```
Always check `error` first.
- `error: false` → use `data`.
- `error: true` → read `error_detail.code` and `error_detail.message`. The exit code is also
non-zero.
With `jq`:
```bash
out=$(emcli list --account gmail --new) || true
echo "$out" | jq -e '.error == false' >/dev/null && echo "$out" | jq '.data.messages'
```
Error codes you may get back: `config`, `db`, `network`, `auth`, `policy`, `not_found`, `usage`.
A `policy` error means the user's rules blocked the action; `not_found` is returned both for
missing mail **and** for mail you are not allowed to see — treat them the same.
## The core workflow: process new mail
```bash
ACC=gmail # the account name the user gave you
# 1. See what's new (unprocessed). Headers only — cheap.
emcli list --account "$ACC" --new --limit 20
# 2. For a message of interest, fetch the full body + attachments.
emcli get --account "$ACC" --uid 70314
# 3. Do the work (summarize, extract, draft a reply, etc.).
# 4. Mark it processed so it stops showing under --new. This is deliberate — do it
# only when you've actually handled the message.
emcli ack --account "$ACC" --uid-list 70314
```
Acking is the **only** command that changes state; `list`/`get`/`search` never do. Ack is safe to
repeat and order-independent (you can ack several UIDs: `--uid-list 70314,70315,70320`).
## Sending mail
```bash
emcli send --account "$ACC" \
--to alice@example.com --subject "Hello" --body "Plain-text body."
# multiple recipients (repeat or comma-separate), cc/bcc, attachments:
emcli send --account "$ACC" --to a@x.com --to b@x.com --cc boss@x.com \
--subject "Report" --body "see attached" --attach ./report.pdf
# reply that threads correctly off a message you can see:
emcli send --account "$ACC" --to a@x.com --subject "Re: Hi" --body "thanks" \
--reply-to 70314 --folder INBOX
```
Sending only works on read-write accounts. If you get `policy` / `ro_mode`, the account is
read-only — tell the user; do not attempt another account without their say-so.
## Command quick reference
| Command | Purpose |
|---|---|
| `emcli list --account A [--folder F] [--new] [--limit N] [--before U] [--since U]` | Message headers, newest first |
| `emcli get --account A [--folder F] --uid U` | One full message (body + attachments) |
| `emcli search --account A [--folder F] [--from X] [--subject-contains X] [--text X] [--since-date D] [--before-date D]` | Server-side search |
| `emcli ack --account A [--folder F] --uid-list U1,U2` | Mark message(s) processed |
| `emcli send --account A --to X [--cc X] [--bcc X] --subject S --body B [--attach P]… [--reply-to U]` | Send / reply |
Defaults: `--folder INBOX`, `--limit 50` (max 500). Dates are RFC 3339 (e.g.
`2026-06-01T00:00:00Z`). UIDs come from `list`/`search` output.
**Full reference** (every flag, exact JSON shapes for each command, attachment encoding, error
codes, and the enforcement rules): [references/commands.md](references/commands.md).
## Enforcement awareness — work *with* the rules
The user configures these; you cannot change them and shouldn't try.
- **Read-only (RO) accounts** reject `send` (`policy` / `ro_mode`).
- **Inbound whitelist / subject filter:** mail from disallowed senders (or non-matching subjects)
is invisible — it won't appear in `list`/`search`, and `get`/`ack` on it return `not_found`.
- **Outbound whitelist:** if any recipient isn't allowed, the **whole** send is blocked
(`policy` / `whitelist_out`) — nothing is sent. Don't retry by dropping recipients silently;
surface it to the user.
## Do / Don't
- ✅ Check `error` on every call; report `policy`/`not_found`/`auth` outcomes plainly to the user.
- ✅ `get` to read, then `ack` only after you've truly processed a message.
- ✅ Ask the user for the account name; keep bodies plain text.
- ❌ Don't read, print, or invent `EMCLI_KEY` or any password.
- ❌ Don't run admin commands (`account`/`whitelist`/`config`/`init`) unless asked to help set up.
- ❌ Don't treat a blocked send or filtered message as a bug to route around — it's the user's policy.
+172
View File
@@ -0,0 +1,172 @@
# emcli agent command reference
The five agent commands you may use. Each prints **one** JSON object to stdout and sets a matching
exit code (0 success, non-zero error). All take `--account <name>`; most take `--folder` (default
`INBOX`).
## The JSON envelope
```json
{ "error": false, "error_detail": {}, "data": { } }
```
- `error` — boolean. Check it first.
- `error_detail``{}` on success; `{ "code": "...", "message": "..." }` on failure.
- `data` — command-specific payload (below).
**Error codes:** `config` (key/config), `db`, `network`, `auth`, `policy` (blocked by a rule),
`not_found` (missing **or** filtered/invisible mail), `usage` (bad/missing flag).
---
## `list` — headers, newest first
```
emcli list --account A [--folder F] [--new] [--limit N] [--before U] [--since U]
```
| Flag | Default | Meaning |
|---|---|---|
| `--folder` | `INBOX` | Mailbox |
| `--new` | off | Only messages not yet `ack`ed |
| `--limit` | `50` | Max results (capped at 500) |
| `--before <uid>` | — | Only UIDs lower than this (page to older mail) |
| `--since <uid>` | — | Only UIDs higher than this (page to newer mail) |
`data`:
```json
{ "messages": [
{ "uid": 70314,
"from": "\"Boss\" <boss@example.com>",
"to": "<you@example.com>",
"subject": "Hello",
"date": "Mon, 22 Jun 2026 17:00:30 +0000",
"message_id": "abc@example.com",
"has_attachments": false } ] }
```
Headers only — no body is downloaded. `data.messages` is `[]` when nothing matches.
---
## `get` — one full message
```
emcli get --account A [--folder F] --uid U
```
`data`:
```json
{ "uid": 70314,
"from": "\"Boss\" <boss@example.com>",
"to": "<you@example.com>",
"subject": "Hello",
"date": "Mon, 22 Jun 2026 17:00:30 +0000",
"message_id": "abc@example.com",
"body_text": "the decoded plain-text body…",
"attachments": [
{ "name": "report.pdf", "size": 20480, "mime": "application/pdf", "content_b64": "JVBERi0…" } ] }
```
- `body_text` is the decoded plain-text part.
- Each attachment's bytes are base64 in `content_b64`; decode to recover the file
(`echo "$b64" | base64 -d > report.pdf`). `size` is the decoded byte length.
- `get` does **not** acknowledge the message.
- A filtered/invisible or missing UID returns `error: true`, code `not_found`.
---
## `search` — server-side search (whole folder)
```
emcli search --account A [--folder F] [--from X] [--subject-contains X] [--text X] \
[--since-date D] [--before-date D] [--limit N]
```
| Flag | Meaning |
|---|---|
| `--from` | Sender contains |
| `--subject-contains` | Subject contains |
| `--text` | Full-text |
| `--since-date` / `--before-date` | RFC 3339 bounds, e.g. `2026-06-01T00:00:00Z` |
| `--limit` | Max results (default 50) |
`data` shape is identical to `list` (`{ "messages": [ … ] }`). Searches the whole folder regardless
of new/acked state. Filtered mail never appears.
---
## `ack` — mark message(s) processed
```
emcli ack --account A [--folder F] --uid-list U1,U2,U3
```
`data`:
```json
{ "acked": [70314, 70315, 70320] }
```
- The **only** command that changes state. Call it after you've actually handled a message.
- Idempotent and order-independent; batch multiple UIDs comma-separated.
- After ack, those UIDs no longer appear under `list --new`.
- You cannot ack a message you aren't allowed to see — returns `not_found`.
---
## `send` — send or reply (RW accounts only)
```
emcli send --account A --to X [--cc X] [--bcc X] --subject S --body B \
[--attach P]… [--reply-to U [--folder F]]
```
| Flag | Meaning |
|---|---|
| `--to` / `--cc` / `--bcc` | Recipients — repeat the flag or comma-separate (`--to a@x,b@x`) |
| `--subject` | Subject |
| `--body` | Plain-text body |
| `--attach` | File path to attach (repeatable) |
| `--reply-to <uid>` | Thread the reply onto this source message |
| `--folder` | Folder of the `--reply-to` source (default `INBOX`) |
`data`:
```json
{ "sent": true, "recipients": ["alice@example.com", "boss@example.com"] }
```
Blocked sends return `error: true`, code `policy`:
- `ro_mode` — the account is read-only; it cannot send.
- `whitelist_out` — a recipient isn't on the outbound whitelist; the **whole** send is blocked and
nothing was sent. Don't silently drop recipients — tell the user.
`--reply-to` reads the source message's `Message-ID`/`References` so the reply threads. The source
is subject to the inbound filter: a filtered/missing source returns `not_found`.
---
## Enforcement rules (set by the user; you can't change them)
- **Mode:** `RO` accounts reject `send`. `RW` can read and send.
- **Inbound whitelist / subject filter:** disallowed mail is invisible everywhere (`list`/`search`
omit it; `get`/`ack` return `not_found`). You can't tell a filtered message from a non-existent
one — by design.
- **Outbound whitelist:** every recipient (to+cc+bcc) must match, or the send is blocked whole.
- **Address matching:** case-insensitive; an entry `@domain.com` matches any address at that
domain; otherwise an exact-address match.
## Parsing tips
```bash
# Guard on success, then read data:
out=$(emcli list --account gmail --new --limit 10)
if echo "$out" | jq -e '.error == false' >/dev/null; then
echo "$out" | jq -r '.data.messages[] | "\(.uid)\t\(.subject)"'
else
echo "$out" | jq -r '.error_detail | "\(.code): \(.message)"' >&2
fi
# Save an attachment from get:
emcli get --account gmail --uid 70314 \
| jq -r '.data.attachments[0].content_b64' | base64 -d > report.pdf
```
+62
View File
@@ -0,0 +1,62 @@
# Installing the emcli binary
The skill's `scripts/install.sh` downloads a prebuilt binary from the project's release assets.
## Quick install
```bash
bash scripts/install.sh
```
It detects your OS (`linux`/`darwin`/`windows`) and architecture (`amd64`/`arm64`), downloads the
matching asset, verifies its SHA-256 checksum when a `checksums.txt` is published, makes it
executable, and confirms it runs.
## Options (environment variables)
| Variable | Default | Purpose |
|---|---|---|
| `EMCLI_VERSION` | `v0.4.0` | Release tag to fetch |
| `EMCLI_BASE_URL` | `https://gitea.dcglab.co.uk/steve/emcli` | Repo base URL |
| `EMCLI_INSTALL_DIR` | `$HOME/.local/bin` | Install location |
Example — install a specific version to a system directory:
```bash
EMCLI_VERSION=v0.4.0 EMCLI_INSTALL_DIR=/usr/local/bin bash scripts/install.sh
```
## Release asset naming
The release publishes one binary per platform plus a checksum file:
```
emcli_0.4.0_linux_amd64
emcli_0.4.0_linux_arm64
emcli_0.4.0_darwin_amd64
emcli_0.4.0_darwin_arm64
emcli_0.4.0_windows_amd64.exe
checksums.txt # sha256, one "<sum> <asset>" line per asset
```
> `v0.4.0` and these assets are placeholders until the first tagged release exists. Update
> `EMCLI_VERSION` (or the default in `install.sh`) once a real release is cut.
## Building from source instead
If you have Go and prefer to build rather than download:
```bash
git clone https://gitea.dcglab.co.uk/steve/emcli
cd emcli
CGO_ENABLED=0 go build -o emcli ./cmd/emcli
# then move ./emcli onto your PATH
```
## After installing
`emcli` needs the `EMCLI_KEY` environment variable (a base64-encoded 32-byte AES key) to touch its
database. For agent use, the **orchestrator provides this** — the agent should not generate or read
it. A human setting up emcli for the first time generates one with
`head -c 32 /dev/urandom | base64` and saves it securely. See the project User Manual for full admin
setup.
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
#
# install.sh — download the emcli binary from the project's release assets and
# put it on your PATH. Detects OS/arch, verifies the checksum when available.
#
# Usage:
# bash install.sh
#
# Environment overrides:
# EMCLI_VERSION release tag to fetch (default: v0.4.0)
# EMCLI_BASE_URL repo base URL (default: https://gitea.dcglab.co.uk/steve/emcli)
# EMCLI_INSTALL_DIR where to put the binary (default: $HOME/.local/bin)
#
# NOTE: v0.4.0 and its release assets are placeholders until the first tagged
# release is published. The asset naming below is the scheme the release will use:
# emcli_<version>_<os>_<arch>[.exe] e.g. emcli_0.4.0_linux_amd64
# checksums.txt (sha256, one "<sum> <asset>" line per asset)
set -euo pipefail
VERSION="${EMCLI_VERSION:-v0.4.0}"
BASE_URL="${EMCLI_BASE_URL:-https://gitea.dcglab.co.uk/steve/emcli}"
INSTALL_DIR="${EMCLI_INSTALL_DIR:-$HOME/.local/bin}"
die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }
# --- detect OS -------------------------------------------------------------
case "$(uname -s)" in
Linux) OS=linux ;;
Darwin) OS=darwin ;;
MINGW*|MSYS*|CYGWIN*) OS=windows ;;
*) die "unsupported OS: $(uname -s)" ;;
esac
# --- detect arch -----------------------------------------------------------
case "$(uname -m)" in
x86_64|amd64) ARCH=amd64 ;;
arm64|aarch64) ARCH=arm64 ;;
*) die "unsupported architecture: $(uname -m)" ;;
esac
EXT=""
[ "$OS" = windows ] && EXT=".exe"
VER_NO_V="${VERSION#v}"
ASSET="emcli_${VER_NO_V}_${OS}_${ARCH}${EXT}"
URL="${BASE_URL}/releases/download/${VERSION}/${ASSET}"
DEST="${INSTALL_DIR}/emcli${EXT}"
# --- pick a downloader -----------------------------------------------------
download() { # download <url> <dest>
if command -v curl >/dev/null 2>&1; then
curl -fSL "$1" -o "$2"
elif command -v wget >/dev/null 2>&1; then
wget -qO "$2" "$1"
else
die "need curl or wget to download"
fi
}
mkdir -p "$INSTALL_DIR"
printf 'Downloading %s\n from %s\n' "$ASSET" "$URL"
download "$URL" "$DEST" || die "download failed (is ${VERSION} published yet?)"
chmod +x "$DEST"
# --- verify checksum if a checksums.txt is published -----------------------
if command -v sha256sum >/dev/null 2>&1; then
sums="$(mktemp)"
if download "${BASE_URL}/releases/download/${VERSION}/checksums.txt" "$sums" 2>/dev/null; then
want="$(awk -v a="$ASSET" '$2 == a || $2 == "*"a {print $1}' "$sums" | head -n1)"
if [ -n "$want" ]; then
got="$(sha256sum "$DEST" | awk '{print $1}')"
[ "$want" = "$got" ] || die "checksum mismatch for ${ASSET} (expected ${want}, got ${got})"
echo "checksum ok"
fi
fi
rm -f "$sums"
fi
printf 'Installed emcli to %s\n' "$DEST"
# --- PATH hint -------------------------------------------------------------
case ":$PATH:" in
*":$INSTALL_DIR:"*) ;;
*) printf 'Note: %s is not on your PATH. Add it, e.g.:\n export PATH="%s:$PATH"\n' "$INSTALL_DIR" "$INSTALL_DIR" ;;
esac
# --- confirm it runs -------------------------------------------------------
"$DEST" version || die "binary downloaded but failed to run"
echo "Done. Remember: emcli needs EMCLI_KEY set in the environment to do anything."