CLI
@agreely/cli is the consent gate as a command-line tool. One binary, two modes:
- Humans get colored output and an interactive wizard (at a TTY).
- Agents get pure JSON on stdout and stable exit codes (a pipe or
--json).
It is a thin shell over @agreely/sdk: it never
reimplements the HTTP, decision, or normalization logic. It resolves auth, picks
a mode, calls the SDK, and maps the result to an exit code.
Package @agreely/cli v0.1.0, binary agreely, ESM, Node >=18.
Install
npm install -g @agreely/cli
agreely --help
agreely --version # -> 0.1.0
Agent-native: one env var + --json
export AGREELY_API_KEY=agr_live_xxx # the only setup an agent needs
agreely check cust-42 "Email Address" "Marketing Outreach" --json
# -> {"decision":"allow","status":"active","consentRef":"0x…"} exit 0
Set the key once via AGREELY_API_KEY (no prompt, no keychain needed), pass
--json (or just pipe: a non-TTY stdout auto-triggers agent mode), and branch on
the exit code. category / purpose are sent raw; the server normalizes them.
Global flags
Accepted before or after the subcommand:
--json: force agent mode (pure JSON on stdout).--api-key <key>: discouraged, visible inps.--base-url <url>: override the API base URL.
Environment variables
AGREELY_API_KEY: the agent auth path.AGREELY_BASE_URL: the API base URL.AGREELY_RPC_URL: the RPC fallback forverify --onchain.XDG_CONFIG_HOME: relocates the config directory.
The exit-code table (the agent contract)
The exit code is the contract. It cleanly separates an outage from a denial and a verification failure from an outage, which is the whole point of an agent-native consent gate.
| code | meaning |
|---|---|
0 |
success, or a check ALLOW |
10 |
a clean check DENY, an expected negative, not an error |
2 |
usage error: bad/missing args, invalid input, a server validation error, a config error, and a not-found |
3 |
auth: the key is missing, invalid, revoked, or lacks the required scope |
4 |
unavailable: an Agreely outage (distinct from a deny), and a client-side poll timeout |
5 |
rate-limited: the per-company window was exceeded |
6 |
verification failed: a receipt was checked and did not verify (a tamper). A verdict, not an error |
1 |
an unexpected / uncategorized failure |
check resolves ALLOW to 0 and DENY to 10. The CLI is fail-closed: on an
outage the SDK throws and the CLI exits 4, so a caller can always tell "outage"
from "denied". A DENY's JSON still goes to stdout; a real error keeps stdout clean
and writes a {"error":{"code","message"}} envelope to stderr.
Modes (auto-detected)
| condition | mode | behavior |
|---|---|---|
stdout is a TTY and no --json |
human | colors, the wizard, confirmations |
stdout is not a TTY, or --json |
agent | no prompts ever, pure JSON to stdout, logs/errors to stderr |
In agent mode, stdout carries pure JSON and nothing else; every log, hint, or
error goes to stderr, under a {"error":{"code","message"}} envelope on error. In
human mode the output is colorized with wizards and confirmations, and an error
prints in red as x message. picocolors auto-disables color when not a TTY. A
missing required argument in agent mode is a clear error plus a usage exit (2);
it never hangs waiting on a prompt.
Commands
agreely check <customerId> <category> <purpose>
agreely check --batch <file.json>
agreely catalog
agreely whoami
agreely requests [--status <s>] [--cursor <id>]
agreely request create [--customer <id>] [--to <email>] [--item <item>]... [--valid-until <YYYY-MM-DD>] [--idempotency-key <key>]
agreely request show <requestId>
agreely request cancel <requestId>
agreely request wait <requestId> [--interval <ms>] [--timeout <ms>]
agreely manual-consent create [--customer <id>] [--document-version <id>] [--effective-date <YYYY-MM-DD>] [--valid-until <YYYY-MM-DD>] [--item <item>]... [--pdf <path>] [--upload]
agreely manual-consent claim-link [--customer <id>] [--reference <ref>]
agreely manual-consent revoke <consentRef> [--reason <text>]
agreely manual-consent erase <consentRef> [--reason <text>]
agreely relationship end <customerRef> --reason <text>
agreely verify <receipt.json> [--ipfs] [--onchain] [--rpc-url <url>] [--did-doc <file>]...
agreely login
agreely config set [--api-key <k>] [--base-url <url>]
check
Synchronous decision. category / purpose are sent raw (the server normalizes).
Exit 0 = ALLOW, 10 = DENY, 4 = outage.
agreely check cust-42 "Email Address" "Marketing Outreach" --json
# {"decision":"allow","status":"active","consentRef":"0x…"} exit 0
# {"decision":"deny","status":"revoked","consentRef":"0x…"} exit 10
In human mode: OK ALLOW … or DENY …. In agent mode the JSON is
{decision, status, consentRef?}.
Batch mode: --batch <file.json> reads a JSON array of
{customerRef, category, purpose} objects, calls checkBatch() once, and prints
a decisions table (human) or a JSON array (agent, one
{customerRef, category, purpose, decision, status, consentRef?} per cell). Exit is
0 when all cells allow, 10 when any denies, 4 on an outage. This is the
tool for gating a list display (N customers x M fields = N x M checks) in one
round-trip.
agreely check --batch ./cells.json --json
# [{"customerRef":"cust-42","category":"Email Address","purpose":"Marketing Outreach","decision":"allow","status":"active","consentRef":"0x…"}]
catalog
Lists the company's active catalog. JSON {catalog: entries}.
whoami
Verifies the key server-side by calling GET /v1/whoami: it reports the scopes
the server actually verifies, not a local guess. A bad key exits 3. It never
prints the secret (masked: first 8 + last 4 characters). JSON:
{authenticated, apiKeyMasked, apiKeySource, baseUrl, scopes}.
requests
Lists requests, cursor pagination. --status accepts
pending|approved|refused|expired|revoked_before_action (an invalid value is a
usage error). The JSON emits the raw page {items, nextCursor}.
request create
Dual mode. Non-interactive when agent mode is active or any scriptable flag is
present: the CLI then validates (customer required, valid email, YYYY-MM-DD date,
at least one item). At a human TTY with no flags, a wizard guides catalog
selection, collects the customer, recipient, and valid-until, validates each, and
confirms before issuing.
agreely request create \
--customer cust-42 --to [email protected] \
--item "Email Address:Marketing Outreach" --item 4b082452-… \
--valid-until 2030-01-01 --idempotency-key issue-2026-001 --json
# -> the raw IssuedRequest
An --item is either a catalog entry id or category:purpose. Reuse
--idempotency-key to make a retry safe: a replay returns the original request,
with no double-issue and no double-email.
request show
requestId must match ^0x[0-9a-f]{64}$. JSON: the raw record.
request cancel
Cancels a pending request by its requestId (scope issue). The call is
idempotent: an already-terminal request is not an error; the exit stays 0
with cancelled: false. JSON: {requestId, status, cancelled}.
agreely request cancel 0x… --json
# {"requestId":"0x…","status":"revoked_before_action","cancelled":true} exit 0
request wait
Polls until the request settles
(approved|refused|expired|revoked_before_action). A timeout exits 4.
--interval defaults to 2000 ms, --timeout defaults to 120000 ms. JSON: the
raw settled record.
agreely request wait 0x… --timeout 300000 --json
manual-consent
Scope attest. Records a consent obtained offline (paper or signed PDF).
create: the PDF is hashed locally (sha256); by default only pdfSha256
("0x" + hash) is sent. The raw bytes (base64) leave the machine only with
--upload. All of these are required: --customer, --document-version,
--effective-date, --valid-until, at least one --item, and --pdf. JSON: the
raw ManualConsentResult.
agreely manual-consent create \
--customer cust-42 --document-version dv-2026-05 \
--effective-date 2026-05-01 --valid-until 2031-05-01 \
--item "Email Address:Marketing Outreach" \
--pdf ./signed.pdf --json
# the hash alone leaves; add --upload to also send the bytes
claim-link: --customer required. JSON: the raw ClaimLink
{claimUrl, token, expiresAt}.
revoke: consentRef matches ^0x[0-9a-f]+$ (case-insensitive). JSON: the raw
revocation {consentRef, revoked, alreadyRevoked}.
erase: erases (crypto-shreds) a manual consent. JSON: the raw erasure
{consentRef, erased, alreadyErased}.
relationship end
Scope relationship. Attests that a customer relationship is over (Law 25 art. 23,
"les fins sont accomplies"). <customerRef> is your own customer reference (the
same one used by check), scoped to the key's company. --reason is required:
a missing or blank reason exits with an error, never a silent end. The call is
idempotent (re-ending an already-ended relationship is a success; the original
endedAt and endedBy stand). It is a lifecycle overlay: it never revokes or
erases any consent. JSON: {customerRef, status, endedAt, endedBy}.
agreely relationship end cust-42 --reason "Account closed, purposes fulfilled" --json
# {"customerRef":"cust-42","status":"ended","endedAt":"2026-06-26T15:04:05Z","endedBy":"company"} exit 0
verify
Offline-first receipt verification. A thin wrapper over the SDK static
Agreely.verifyReceipt, surfacing the same honest statuses: it does not
re-derive verdicts.
In human mode, verify prints an honesty matrix with five rows,
companySignature, citizenAssertion, disclosureCopy, documentAnchor, and
cellLabelBinding, each pass / fail / unavailable / unsupported /
skipped, plus a top verdict (VERIFIED / PARTIAL / UNAVAILABLE / FAILED)
and notes. The cellLabelBinding row is the field to read before trusting a
displayed label. In agent mode, it emits the raw ReceiptVerification.
agreely verify ./receipt.json --json
Flags:
--ipfs: enables thedisclosureCopycheck (fetches the IPFS copy and compares it todisclosureHash); otherwiseskipped.--onchain: enables the on-chaindocumentAnchorcheck and requires an RPC URL via--rpc-urlorAGREELY_RPC_URL(else a usage error).--did-doc <file>(repeatable): installs a local DID-document resolver, makingverifyfully air-gapped. An unknown DID then resolves tounavailable(exit4) rather than reaching out. Without--did-doc, the signature and assertion checks do one HTTPS DID resolution by default.
Exit 4 (unavailable) versus exit 6 (failed)
This is the honest core of the verifier. An unresolvable DID (the issuer or citizen DID cannot be resolved) is exit 4 (unavailable), not exit 6. Exit 6 is reserved for an active cryptographic failure: a tamper or a wrong key. A valid receipt, during a DID-resolution outage, must never look like a forgery.
login
Interactive only. In agent mode, login errors and points you to config set
or AGREELY_API_KEY. It prompts for the key and an optional base URL, verifies
the key against the live API before persisting (it never stores a key that does
not work), then stores it in the OS keychain, falling back to the 0600 config
file.
config set
Non-interactive: it never prompts and never verifies. It requires at least one of
--api-key and --base-url, and stores the credentials.
agreely config set --api-key agr_live_xxx --base-url https://api.agreely.ca
Auth precedence
The CLI never prompts for a key. Resolution order:
--api-key flag (discouraged, visible in `ps`)
> AGREELY_API_KEY env (the agent path: one var, no prompt)
> OS keychain (keytar, service "agreely")
> ${XDG_CONFIG_HOME|~/.config}/agreely/config.json (mode 0600)
A missing key throws a usage error (exit 2): "No API key. Set AGREELY_API_KEY,
run 'agreely login', or pass --api-key." The default API base URL shown is
https://api.agreely.ca.
Base URL: --base-url > AGREELY_BASE_URL > stored config > the SDK default. If
the optional keytar native module is unavailable, login / config set fall
back to the 0600 config file.
Degrade is omitted (by design)
The CLI is fail-closed and maps an outage to exit 4. The fail-open /
two-gate / break-glass degrade policy is intentionally omitted from CLI v1:
it is a long-lived application/SDK-integration concern (with an audit sink and a
bounded window), not something a one-shot CLI invocation can persist or audit
sensibly. Configure it where the SDK is embedded.