Compare commits
6 Commits
v0.4.0
...
c946516d01
| Author | SHA1 | Date | |
|---|---|---|---|
| c946516d01 | |||
| b3390a0a20 | |||
| 1b2fe99055 | |||
| 7087533644 | |||
| 93dbebb982 | |||
| 68a29ad5c7 |
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user