From 7ad4f1adc258ba3704ac24d3f203bd9b427ee17f Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Mon, 22 Jun 2026 20:25:08 +0100 Subject: [PATCH] feat(skill): add emcli Agent Skill (agentskills.io standard) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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___) 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) --- README.md | 4 + skills/emcli/SKILL.md | 146 +++++++++++++++++++++++ skills/emcli/references/commands.md | 172 ++++++++++++++++++++++++++++ skills/emcli/references/install.md | 62 ++++++++++ skills/emcli/scripts/install.sh | 91 +++++++++++++++ 5 files changed, 475 insertions(+) create mode 100644 skills/emcli/SKILL.md create mode 100644 skills/emcli/references/commands.md create mode 100644 skills/emcli/references/install.md create mode 100755 skills/emcli/scripts/install.sh diff --git a/README.md b/README.md index ffa9c32..76b3c5d 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,7 @@ emcli doctor # confirm it connects 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. + +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. diff --git a/skills/emcli/SKILL.md b/skills/emcli/SKILL.md new file mode 100644 index 0000000..3997a89 --- /dev/null +++ b/skills/emcli/SKILL.md @@ -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. diff --git a/skills/emcli/references/commands.md b/skills/emcli/references/commands.md new file mode 100644 index 0000000..94748b4 --- /dev/null +++ b/skills/emcli/references/commands.md @@ -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 `; 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 ` | — | Only UIDs lower than this (page to older mail) | +| `--since ` | — | Only UIDs higher than this (page to newer mail) | + +`data`: +```json +{ "messages": [ + { "uid": 70314, + "from": "\"Boss\" ", + "to": "", + "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\" ", + "to": "", + "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 ` | 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 +``` diff --git a/skills/emcli/references/install.md b/skills/emcli/references/install.md new file mode 100644 index 0000000..424d487 --- /dev/null +++ b/skills/emcli/references/install.md @@ -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 " " 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. diff --git a/skills/emcli/scripts/install.sh b/skills/emcli/scripts/install.sh new file mode 100755 index 0000000..02d46d0 --- /dev/null +++ b/skills/emcli/scripts/install.sh @@ -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___[.exe] e.g. emcli_0.4.0_linux_amd64 +# checksums.txt (sha256, one " " 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 + 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."