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
+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.