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:
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user