6 Commits

Author SHA1 Message Date
steve c946516d01 chore(skill): point installer default at v0.4.1
release / release (push) Successful in 3m21s
Bump EMCLI_VERSION default (install.sh + AGENTIC-MANUAL.md + RELEASING.md) so
agents install the v0.4.1 binary (help for all commands, SMTP-port form default,
skill split). Drop the stale "placeholder until first release" note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:17:02 +01:00
steve b3390a0a20 fix(tui): default SMTP port to 465 in the account form
NewAccountForm prefilled defaults for mode, IMAP port, and both securities but
left SMTP port blank. Default it to 465 to match `account add --smtp-port`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:15:25 +01:00
steve 1b2fe99055 feat(cli): add help for all commands
emcli had only raw flag usage and no command listing; `--help` on agent commands
even emitted a JSON error envelope and exited 2. Add real help:

- Top-level `emcli` / `help` / `-h` / `--help` prints a grouped command catalogue
  (agent vs admin) with one-line summaries and the EMCLI_KEY/EMCLI_DB env vars.
- `emcli help <command>` prints that command's synopsis + summary.
- `emcli <command> --help` prints synopsis + summary + flags and exits 0. Agent
  commands keep stdout JSON-free (usage goes to stderr); admin commands print to
  stdout. Help works without EMCLI_KEY (no DB access).
- help.go holds the command catalogue; flag.ErrHelp is handled as success, and
  admin handlers short-circuit help before opening the store.

Unknown commands still error (exit 2). Full suite passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:11:40 +01:00
steve 7087533644 docs(skill): split setup into AGENTIC-MANUAL.md; keep SKILL.md lean
The SKILL.md body loads into context on every activation, so one-time install/
setup prose was wasted context once emcli is running. Move it out:

- New AGENTIC-MANUAL.md: get-the-files bootstrap, binary install (incl. options
  and build-from-source, folding in the old references/install.md), EMCLI_KEY,
  account discovery. Fetched only during first-time setup.
- SKILL.md trimmed (182→~145 lines) to the recurring path: security model, a short
  "Files & first run" pointer + per-session preflight, the list/get/ack/send
  workflow, JSON envelope, command table, enforcement, do/don't.
- Remove references/install.md (folded in); fix RELEASING.md pointer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:01:46 +01:00
steve 93dbebb982 docs(skill): make SKILL.md self-bootstrapping from the repo
An agent pointed at the repo may load only SKILL.md and then guess a wrong path
for the installer (it fetched /scripts/install.sh at repo root → 404; the file is
under skills/emcli/). Fix:

- Add a "First: get this skill's files" section: the supporting scripts/ and
  references/ files, the absolute raw base URL to fetch them, and the Gitea
  contents API to enumerate the directory.
- Install step now gives an absolute-URL fetch-then-run for the only-SKILL.md case,
  keeping `bash scripts/install.sh` for the bundled case.
- State that every scripts/… and references/… path is relative to the skill dir and
  resolvable against the raw base URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:51:15 +01:00
steve 68a29ad5c7 docs: mark CI release verified; note releases must be public for the installer
The Gitea Actions workflow published v0.4.0 successfully, so drop the "untested"
caveat. Document that release assets download anonymously — the repo/releases must
be public or install.sh gets a 404 (private repos 404 unauthenticated downloads).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:40:45 +01:00
14 changed files with 352 additions and 95 deletions
+2 -2
View File
@@ -4,8 +4,8 @@
#
# Requires: Gitea Actions enabled with a runner that has Go, make, curl, and jq
# (the actions/checkout + actions/setup-go steps need the instance's Actions proxy).
# This workflow has not been exercised against this repo's runners yet; if a step
# is unavailable on your runner, the same result comes from `make release && make
# Verified: this workflow published v0.4.0 on this instance. If a step is ever
# unavailable on your runner, the same result comes from `make release && make
# publish` locally (see RELEASING.md).
name: release
on:
+9 -5
View File
@@ -39,14 +39,18 @@ git push origin v0.4.0 # (push via the tokenized HTTPS URL this repo uses)
```
The workflow runs `make release` and uploads the assets to the release via the Gitea API. It needs
Gitea Actions enabled with a runner that provides Go, make, curl, and jq. It hasn't been exercised
against this repo's runners yet — if it doesn't fit your runner setup, fall back to Option A.
Gitea Actions enabled with a runner that provides Go, make, curl, and jq. This is how v0.4.0 was
published. If it ever doesn't fit your runner setup, fall back to Option A.
> Note: release asset downloads are anonymous, so the repository (or at least its releases) must be
> public for `skills/emcli/scripts/install.sh` to fetch binaries without a token. A private repo
> returns 404 to unauthenticated downloads.
## After a release
The skill installer defaults to `EMCLI_VERSION=v0.4.0`. When you cut a different version, either
publish under that tag or update the default in `skills/emcli/scripts/install.sh` (and the note in
`skills/emcli/references/install.md`).
The skill installer defaults to `EMCLI_VERSION=v0.4.1`. When you cut a different version, either
publish under that tag or update the default in `skills/emcli/scripts/install.sh` (and the options
table in `skills/emcli/AGENTIC-MANUAL.md`).
## Versioning
+4
View File
@@ -524,6 +524,10 @@ running non-interactively.
## 12. Command cheat sheet
```
# Help
emcli # or: emcli help / emcli --help — list all commands
emcli <command> --help # usage and flags for one command
# Admin
emcli init # create DB + add first account (form)
emcli account add [flags | none for form] # add an account
+24 -2
View File
@@ -12,8 +12,12 @@ import (
// runAccount handles `account add|list`. Human-readable output (never JSON).
func runAccount(args []string, out, errOut io.Writer) int {
if len(args) == 0 {
fmt.Fprintln(errOut, "usage: emcli account <add|list>")
if len(args) == 0 || helpRequested(args[0]) {
printCmdUsage(out, "account")
fmt.Fprintln(out, "\nSubcommands: add, edit, remove, list")
if len(args) > 0 {
return 0 // explicit --help
}
return 2
}
sub, rest := args[0], args[1:]
@@ -188,6 +192,13 @@ func auditList(st *store.Store, account string, limit int, out io.Writer) error
// runConfig handles `config set <key> <value>` and `config get <key>`.
func runConfig(args []string, out, errOut io.Writer) int {
if len(args) == 0 || helpRequested(args[0]) {
printCmdUsage(out, "config")
if len(args) > 0 {
return 0
}
return 2
}
if len(args) < 2 {
fmt.Fprintln(errOut, "usage: emcli config <set|get> <key> [value]")
return 2
@@ -236,6 +247,10 @@ func runConfig(args []string, out, errOut io.Writer) int {
// runAudit handles `audit list [--account <name>] [--limit N]`.
func runAudit(args []string, out, errOut io.Writer) int {
if len(args) > 0 && helpRequested(args[0]) {
printCmdUsage(out, "audit")
return 0
}
if len(args) == 0 || args[0] != "list" {
fmt.Fprintln(errOut, "usage: emcli audit list [--account <name>] [--limit N]")
return 2
@@ -262,6 +277,13 @@ func runAudit(args []string, out, errOut io.Writer) int {
// runWhitelist handles `whitelist <in|out> add --account NAME --address A`.
func runWhitelist(args []string, out, errOut io.Writer) int {
if len(args) == 0 || helpRequested(args[0]) {
printCmdUsage(out, "whitelist")
if len(args) > 0 {
return 0
}
return 2
}
if len(args) < 2 {
fmt.Fprintln(errOut, "usage: emcli whitelist <in|out> <add|remove|list> [flags]")
return 2
+83
View File
@@ -0,0 +1,83 @@
package cli
import (
"flag"
"fmt"
"io"
)
type cmdHelp struct {
name string
synopsis string
summary string
}
// agentCmds emit machine-readable JSON; adminCmds are human-readable.
var agentCmds = []cmdHelp{
{"list", "list --account <name> [--folder F] [--new] [--limit N] [--before U] [--since U]", "List message headers, newest first."},
{"get", "get --account <name> [--folder F] --uid <uid>", "Fetch one full message (body + attachments)."},
{"search", "search --account <name> [--folder F] [--from A] [--subject-contains S] [--text S] [--since-date D] [--before-date D] [--limit N]", "Server-side IMAP search."},
{"ack", "ack --account <name> [--folder F] --uid-list U1,U2,…", "Mark message(s) processed."},
{"send", "send --account <name> --to A… [--cc A…] [--bcc A…] --subject S --body B [--attach P]… [--reply-to U [--folder F]]", "Send or reply (RW accounts only)."},
}
var adminCmds = []cmdHelp{
{"init", "init", "Create the database and add the first account (interactive)."},
{"account", "account <add|edit|remove|list> [flags]", "Manage accounts (add/edit accept flags, or run with none for an interactive form)."},
{"whitelist", "whitelist <in|out> <add|remove|list> --account <name> [--address A]", "Manage inbound/outbound whitelists."},
{"config", "config <set|get> <key> [value]", "Get or set global settings (e.g. audit_retention_days)."},
{"audit", "audit list [--account <name>] [--limit N]", "Show recent audit-log entries."},
{"doctor", "doctor [--account <name>]", "Check each account's IMAP/SMTP connectivity and auth."},
{"version", "version", "Print the emcli version."},
{"help", "help [command]", "Show this help, or detailed usage for one command."},
}
func helpIndex() map[string]cmdHelp {
m := make(map[string]cmdHelp, len(agentCmds)+len(adminCmds))
for _, c := range append(append([]cmdHelp{}, agentCmds...), adminCmds...) {
m[c.name] = c
}
return m
}
// helpRequested reports whether an argument is a help flag/word.
func helpRequested(s string) bool {
return s == "help" || s == "-h" || s == "--help"
}
// printMainHelp writes the top-level command catalogue.
func printMainHelp(w io.Writer) {
fmt.Fprint(w, "emcli — guard-railed email gateway for agents\n\n")
fmt.Fprint(w, "Usage:\n emcli <command> [flags]\n\n")
fmt.Fprint(w, "Agent commands (machine-readable JSON on stdout):\n")
for _, c := range agentCmds {
fmt.Fprintf(w, " %-10s %s\n", c.name, c.summary)
}
fmt.Fprint(w, "\nAdmin commands (human-readable):\n")
for _, c := range adminCmds {
fmt.Fprintf(w, " %-10s %s\n", c.name, c.summary)
}
fmt.Fprint(w, "\nRun \"emcli <command> --help\" for a command's flags.\n")
fmt.Fprint(w, "\nEnvironment:\n")
fmt.Fprint(w, " EMCLI_KEY base64-encoded 32-byte AES key; required for any command that uses the database\n")
fmt.Fprint(w, " EMCLI_DB database path (default ~/.config/emcli/emcli.db; %AppData%\\emcli\\emcli.db on Windows)\n")
}
// printCmdUsage writes "Usage: emcli <synopsis>" and the summary for one command.
func printCmdUsage(w io.Writer, name string) {
if h, ok := helpIndex()[name]; ok {
fmt.Fprintf(w, "Usage: emcli %s\n\n%s\n", h.synopsis, h.summary)
return
}
fmt.Fprintf(w, "Usage: emcli %s\n", name)
}
// usageFlags makes a flag set print the command's synopsis/summary followed by
// its flags whenever flag prints usage (on -h/--help or a flag error).
func usageFlags(fs *flag.FlagSet, name string, w io.Writer) {
fs.Usage = func() {
printCmdUsage(w, name)
fmt.Fprintln(w, "\nFlags:")
fs.PrintDefaults()
}
}
+73
View File
@@ -0,0 +1,73 @@
package cli
import (
"strings"
"testing"
)
func TestMainHelpListsAllCommands(t *testing.T) {
// help / --help / -h / no-args all print the command catalogue, exit 0,
// and require no EMCLI_KEY (help must work before any DB access).
for _, args := range [][]string{{"help"}, {"--help"}, {"-h"}, {}} {
code, out, errOut := run(t, args...)
text := out + errOut
if code != 0 {
t.Fatalf("%v: want exit 0, got %d\n%s", args, code, text)
}
for _, want := range []string{
"Usage", "list", "get", "search", "ack", "send",
"account", "whitelist", "config", "audit", "doctor", "version",
"EMCLI_KEY", "EMCLI_DB",
} {
if !strings.Contains(text, want) {
t.Fatalf("%v: help missing %q\n%s", args, want, text)
}
}
}
}
func TestHelpForSpecificCommand(t *testing.T) {
code, out, errOut := run(t, "help", "send")
text := out + errOut
if code != 0 {
t.Fatalf("help send exit=%d", code)
}
if !strings.Contains(text, "Usage: emcli send") || !strings.Contains(text, "--to") {
t.Fatalf("help send missing synopsis:\n%s", text)
}
}
func TestAgentHelpDoesNotEmitJSON(t *testing.T) {
// `list --help` must NOT print a JSON envelope on stdout (an agent parses
// stdout) and must exit 0 — even with no EMCLI_KEY set.
code, out, errOut := run(t, "list", "--help")
if code != 0 {
t.Fatalf("list --help exit=%d (out=%q err=%q)", code, out, errOut)
}
if strings.TrimSpace(out) != "" {
t.Fatalf("agent help must keep stdout clean, got: %q", out)
}
if !strings.Contains(errOut, "Usage: emcli list") || !strings.Contains(errOut, "--account") {
t.Fatalf("list --help should print usage+flags on stderr:\n%s", errOut)
}
}
func TestSendHelpExitsZero(t *testing.T) {
code, _, errOut := run(t, "send", "--help")
if code != 0 || !strings.Contains(errOut, "--to") {
t.Fatalf("send --help: code=%d err=%q", code, errOut)
}
}
func TestAdminCommandHelpExitsZero(t *testing.T) {
for _, c := range []string{"account", "whitelist", "config", "audit", "doctor"} {
code, out, errOut := run(t, c, "--help")
text := out + errOut
if code != 0 {
t.Fatalf("%s --help exit=%d\n%s", c, code, text)
}
if !strings.Contains(text, "Usage: emcli "+c) {
t.Fatalf("%s --help missing usage line:\n%s", c, text)
}
}
}
+4
View File
@@ -73,6 +73,10 @@ func editInteractive(st *store.Store, name string, out, errOut io.Writer) int {
// runInit creates/opens the DB and adds the first account via the TUI form,
// seeding a default audit retention if unset.
func runInit(args []string, out, errOut io.Writer) int {
if len(args) > 0 && helpRequested(args[0]) {
printCmdUsage(out, "init")
return 0
}
st, err := openStore()
if err != nil {
fmt.Fprintf(errOut, "emcli: %v\n", err)
+21 -3
View File
@@ -1,6 +1,7 @@
package cli
import (
"errors"
"flag"
"fmt"
"io"
@@ -70,8 +71,12 @@ func newDepsLive(st *store.Store, out io.Writer) Deps {
func runDoctor(args []string, out, errOut io.Writer) int {
fs := flag.NewFlagSet("doctor", flag.ContinueOnError)
fs.SetOutput(errOut)
usageFlags(fs, "doctor", errOut)
account := fs.String("account", "", "check only this account")
if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return 2
}
st, err := openStore()
@@ -89,9 +94,14 @@ func runDoctor(args []string, out, errOut io.Writer) int {
// Run routes a command line and returns an exit code.
func Run(args []string, out, errOut io.Writer) int {
if len(args) == 0 {
fmt.Fprintln(errOut, "emcli: no command given")
return 2
if len(args) == 0 || helpRequested(args[0]) {
// `emcli`, `emcli help`, `emcli -h`, `emcli --help`, and `emcli help <cmd>`.
if len(args) >= 2 {
printCmdUsage(out, args[1])
} else {
printMainHelp(out)
}
return 0
}
cmd, rest := args[0], args[1:]
switch cmd {
@@ -121,6 +131,7 @@ func Run(args []string, out, errOut io.Writer) int {
func runAgent(cmd string, args []string, out, errOut io.Writer) int {
fs := flag.NewFlagSet(cmd, flag.ContinueOnError)
fs.SetOutput(errOut)
usageFlags(fs, cmd, errOut)
account := fs.String("account", "", "account name")
folder := fs.String("folder", "INBOX", "folder/mailbox")
onlyNew := fs.Bool("new", false, "only new (unacked) messages")
@@ -135,6 +146,9 @@ func runAgent(cmd string, args []string, out, errOut io.Writer) int {
beforeDate := fs.String("before-date", "", "search: RFC3339 date upper bound")
ackUIDs := fs.String("uid-list", "", "ack: comma-separated UIDs")
if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0 // usage already printed to stderr; help isn't an error
}
_ = Failure(CodeUsage, err.Error()).Write(out)
return 2
}
@@ -213,6 +227,7 @@ func (s *stringSlice) Set(v string) error {
func runSend(args []string, out, errOut io.Writer) int {
fs := flag.NewFlagSet("send", flag.ContinueOnError)
fs.SetOutput(errOut)
usageFlags(fs, "send", errOut)
account := fs.String("account", "", "account name")
var to, cc, bcc, attach stringSlice
fs.Var(&to, "to", "recipient (repeatable / comma-separated)")
@@ -224,6 +239,9 @@ func runSend(args []string, out, errOut io.Writer) int {
replyTo := fs.Uint("reply-to", 0, "source UID to reply to (threading)")
folder := fs.String("folder", "INBOX", "folder of the reply source")
if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
_ = Failure(CodeUsage, err.Error()).Write(out)
return 2
}
+3
View File
@@ -203,6 +203,9 @@ func NewAccountForm(initial Fields, editing bool) AccountForm {
if initial.IMAPSecurity == "" {
initial.IMAPSecurity = "tls"
}
if initial.SMTPPort == "" {
initial.SMTPPort = "465"
}
if initial.SMTPSecurity == "" {
initial.SMTPSecurity = "tls"
}
+11
View File
@@ -111,6 +111,17 @@ func TestFieldsFromAccountRoundTrip(t *testing.T) {
}
}
func TestNewAccountFormDefaultsSMTPPort(t *testing.T) {
f := NewAccountForm(Fields{}, false).collect()
if f.SMTPPort != "465" {
t.Fatalf("SMTP port should default to 465, got %q", f.SMTPPort)
}
// The other prefilled defaults must remain intact.
if f.IMAPPort != "993" || f.Mode != "RO" || f.IMAPSecurity != "tls" || f.SMTPSecurity != "tls" {
t.Fatalf("existing defaults regressed: %+v", f)
}
}
func TestAccountFormSubmitValid(t *testing.T) {
m := NewAccountForm(validFields(), false)
// Enter submits; with valid fields the form completes.
+99
View File
@@ -0,0 +1,99 @@
# emcli — setup manual (for agents)
Read this **once**, when emcli isn't set up yet (the binary isn't installed, or you only have
`SKILL.md` from being pointed at the repo). The recurring "how to read and send mail" lives in
`SKILL.md` — don't reload this file for normal use.
## 1. Get this skill's files
This skill is a folder, not a single file: it ships `AGENTIC-MANUAL.md` (this file), a `scripts/`
directory, and a `references/` directory. If you were pointed at the repository and have only
`SKILL.md` in context, fetch what you need before proceeding.
Every `scripts/…` and `references/…` path mentioned anywhere in this skill is **relative to the
skill directory**, whose raw base URL is:
```
https://gitea.dcglab.co.uk/steve/emcli/raw/branch/main/skills/emcli/
```
So a file that isn't already on disk can be fetched from `<base>/<path>`. The files are:
| Path | Purpose |
|---|---|
| `SKILL.md` | The lean, everyday usage guide |
| `AGENTIC-MANUAL.md` | This setup guide (one-time) |
| `scripts/install.sh` | Installs the emcli binary |
| `references/commands.md` | Full agent command reference (flags, JSON shapes, error codes) |
To enumerate the directory programmatically (then recurse into `scripts` and `references`), use the
Gitea contents API: `https://gitea.dcglab.co.uk/api/v1/repos/steve/emcli/contents/skills/emcli`.
## 2. Install the binary
First check whether it's already installed: `emcli version`. If that prints a version, skip to
step 3.
If you have the skill files locally:
```bash
bash scripts/install.sh
```
If you only have this manual (pointed at the repo), fetch the installer first, then run it:
```bash
curl -fsSL https://gitea.dcglab.co.uk/steve/emcli/raw/branch/main/skills/emcli/scripts/install.sh -o emcli-install.sh
bash emcli-install.sh
```
`install.sh` detects your OS/arch, downloads the matching release binary, verifies its SHA-256
checksum, makes it executable in `~/.local/bin` (ensure that's on your PATH), and confirms it runs.
**Installer options** (environment variables):
| Variable | Default | Purpose |
|---|---|---|
| `EMCLI_VERSION` | `v0.4.1` | 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 |
**Build from source instead** (needs Go; the binary is CGO-free):
```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
```
## 3. Confirm the encryption key is present
emcli needs `EMCLI_KEY` (a base64-encoded 32-byte AES key) to touch its database. For agent use,
**the orchestrator that launched you provides it** in the environment.
- Confirm it's set, without printing it: `test -n "$EMCLI_KEY" && echo present`.
- **Never** read, print, log, pass as an argument, or generate this value.
- If it's empty, stop and tell the user: "emcli needs the `EMCLI_KEY` environment variable set by
your orchestrator; I can't read or create it for you."
(For a human setting emcli up the first time: generate one with `head -c 32 /dev/urandom | base64`
and store it securely. Account creation and other admin is the human's job — see the project's
`USER-MANUAL.md`.)
## 4. Find the account(s)
You refer to an account by name (e.g. `gmail`, `work`). Ask the user which account to use. If the
user permits running admin commands, `emcli doctor` lists the configured accounts and checks that
each one connects and authenticates:
```bash
emcli doctor # all accounts
emcli doctor --account gmail
```
Otherwise, just take the account name from the user and start with the workflow in `SKILL.md`.
## You're set up
Installed, key present, account name in hand → switch to `SKILL.md` for the everyday `list` / `get`
/ `search` / `ack` / `send` workflow. You shouldn't need this manual again unless the binary goes
missing.
+15 -16
View File
@@ -23,29 +23,27 @@ sets its exit code to match.
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).
missing, stop and tell the user (see "Files & first run").
- **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)
## Files & first run
1. **Check the binary is available.** Run `emcli version`. If the command is not found, install it:
This skill ships more than this file. Paths like `AGENTIC-MANUAL.md` and `references/commands.md`
are relative to this skill's directory; if one isn't on disk, fetch it from the raw base URL + path:
```bash
bash scripts/install.sh
```
```
https://gitea.dcglab.co.uk/steve/emcli/raw/branch/main/skills/emcli/
```
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.
- **First-time setup** — installing the binary, the `EMCLI_KEY`, finding accounts: read
**`AGENTIC-MANUAL.md`**. Only needed when emcli isn't set up yet.
- **Full command detail** — every flag, JSON shapes, error codes: `references/commands.md`.
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.
**Per-session preflight** (quick): run `emcli version`; if it's not found, set up via
`AGENTIC-MANUAL.md`. Confirm `EMCLI_KEY` is set *without printing it* (`test -n "$EMCLI_KEY"`); if
empty, tell the user their orchestrator must provide it. Then get the account name from the user.
## How to read every result
@@ -124,7 +122,8 @@ 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).
codes, and the enforcement rules): `references/commands.md` — read it from disk, or fetch it from
the raw base URL in "Files & first run" above if you don't have it locally.
## Enforcement awareness — work *with* the rules
-62
View File
@@ -1,62 +0,0 @@
# 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.
+4 -5
View File
@@ -7,18 +7,17 @@
# bash install.sh
#
# Environment overrides:
# EMCLI_VERSION release tag to fetch (default: v0.4.0)
# EMCLI_VERSION release tag to fetch (default: v0.4.1)
# 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
# Release assets follow this naming scheme:
# emcli_<version>_<os>_<arch>[.exe] e.g. emcli_0.4.1_linux_amd64
# checksums.txt (sha256, one "<sum> <asset>" line per asset)
set -euo pipefail
VERSION="${EMCLI_VERSION:-v0.4.0}"
VERSION="${EMCLI_VERSION:-v0.4.1}"
BASE_URL="${EMCLI_BASE_URL:-https://gitea.dcglab.co.uk/steve/emcli}"
INSTALL_DIR="${EMCLI_INSTALL_DIR:-$HOME/.local/bin}"