The Agreely consent gate as a command line tool. One binary, two modes:
- Humans get colored output and an interactive wizard (a TTY).
- Agents get pure JSON on stdout and stable exit codes (a pipe or
--json).
It is a thin shell over @agreely/sdk: the CLI 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. That means it inherits the SDK's
guarantees: no telemetry, no data collection, it only ever talks to the Agreely
API base URL you configure (see Open and auditable).
Once published:
npm install -g @agreely/cli
agreely --helpFor now the CLI consumes the SDK via a local path (file:../agreely-sdk/ts).
Check out the agreely-sdk repo as a sibling of this one and build its TS
package first:
# siblings: ./agreely-cli and ./agreely-sdk
(cd ../agreely-sdk/ts && npm install && npm run build)
npm install && npm run build
node dist/bin.js --helpThe build emits a single runnable dist/bin.js (ESM, shebang). The agreely
bin is declared in package.json.
| 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 |
A missing required argument in agent mode is a clear error + a usage exit - it never hangs waiting on a prompt.
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). - Pass
--json(or just pipe) for machine output. - Branch on the exit code - it is the contract.
category/purposeare sent raw; the server normalizes them.
| code | meaning |
|---|---|
0 |
success / check ALLOW |
2 |
usage or validation error (bad/missing args, invalid input, no credentials) |
3 |
auth - the key is missing, invalid, revoked, or lacks the scope |
4 |
unavailable - an Agreely outage (distinct from a deny) |
5 |
rate-limited - the per-company window was exceeded |
7 |
billing inactive (HTTP 402) - the company's Agreely subscription lapsed. Fail-closed like a deny, but actionable and distinct from an outage |
10 |
check DENY - a clean, expected negative, not an error |
1 |
an unexpected/uncategorized failure |
check resolves ALLOW→0 and DENY→10. The CLI is fail-closed: on an
outage the SDK throws and the CLI exits 4, so a caller can tell "outage" from
"denied". A lapsed company subscription is a 402 that exits 7 (its
envelope code is billing_inactive) - also fail-closed, but distinct from an
outage and actionable: the company must pay to restore service. A DENY's JSON
still goes to stdout; a real error keeps stdout clean and writes a
{"error":{"code","message"}} envelope to stderr.
agreely check <customerId> <category> <purpose> [--json]
agreely catalog [--json]
agreely requests list [--customer <ref>] [--status pending|approved|refused|expired|revoked_before_action] [--limit <n>] [--cursor <id>] [--json] # metadata only; bare `agreely requests ...` is a kept alias
agreely request create [--customer <id> --to <email> (--document <versionId> | --document-code <code>) --valid-until <YYYY-MM-DD>] [--idempotency-key <k>] [--json]
agreely request show <requestId> [--json] # requestId is 0x + 64 hex
agreely request cancel <requestId> [--json] # cancel a pending request (idempotent)
agreely manual-consent create --customer <id> --document-version <id> --effective-date <YYYY-MM-DD> --valid-until <YYYY-MM-DD> --item <catalogId|category:purpose> ... --pdf <path> [--upload] [--json]
agreely manual-consent claim-link --customer <id> [--reference <ref>] [--json]
agreely manual-consent revoke <consentRef> [--reason <text>] [--json]
agreely relationship end <customerRef> --reason <text> [--json] # end a customer relationship (art. 23; idempotent)
agreely relationship revert <customerRef> --reason <text> [--json] # undo a mistaken end (art. 11 / art. 28 correction)
agreely whoami [--json] # server-verified: reports the key's real scopes
agreely login # interactive: store a key in the OS keychain
agreely config set --api-key <k> [--base-url <url>] # non-interactive store (for scripts)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
# {"decision":"allow","status":"necessity","basis":"necessary_for_service"} exit 0Labels are bilingual and accent-tolerant. The category and purpose may be
given in French OR English, with or without accents, and are matched case- and
whitespace-insensitively. English resolves only when the company disclosed an English
label for that cell. An ambiguous or undeclared label fails closed (deny / none), so
pass the label as declared in the catalog when you can.
An allow is not always a consent. status: "necessity" means there is no
consent record: the allow rests on a non-consent lawful basis the company
declared on the catalog cell, and the basis field names it (contract,
necessary_for_service, security_fraud, legal_obligation,
professional_contact). Such a decision has no consentRef and no assurance,
so if you are piping this into a report, read basis: without it a necessity
allow is indistinguishable from a consented one. Agreely records the declared
basis; it does not certify its legal validity.
The other deny statuses you will see are none (no record, and also what an
erased cell reads as), expired, relationship_ended (art. 23: the company
attested the purposes are accomplished; the per-cell consent stays truthfully
active) and sensitive_requires_consent (the company declared the cell sensitive,
so it fails closed to express consent). Treat any status you do not recognise as a
deny and read decision, which is only ever allow or deny.
agreely check --batch cells.json --json # cells.json: [{customerRef, category, purpose}, …]One request for the whole file, exit 0 when every cell allows and exit 10 when any
denies. The server caps a batch at 500 cells: an over-cap file is refused
before the request is sent (exit 2, with the count in the message), so split large
files yourself. The API also allows 120 requests per minute per company (per
company, not per key), and one --batch run is one request, which is the point of
batch mode.
Scriptable (agent) - every required flag present, no prompts:
agreely request create \
--customer cust-42 --to ops@acme.example \
--document 4b082452-… \
--valid-until 2030-01-01 --idempotency-key issue-2026-001 --json
# -> the IssuedRequest: {"requestId":"0x…","status":"pending","deepLink":"…","document":{…},…}Every request is issued under a published consent document (the Law 25 s. 8
disclosure): pass --document <versionId> or --document-code <code> (one, not
both - find them under Consent documents in the company workspace). The
requested (category, purpose) items derive from the document server-side; there
is no --item flag on this command. Reuse --idempotency-key to make a retry
safe - a replay returns the original request, with no double-issue and no
double-email.
Interactive (human) - run it with no flags at a TTY and a wizard collects the document reference, customer, recipient email, and valid-until, validates each, and confirms before issuing.
The offline (company-attested) path: record a consent you gathered out of band
(a signed PDF) under your company's attestation. The result carries
assurance: "company_attested" (the live citizen flow yields citizen_signed).
agreely manual-consent create \
--customer cust-42 --document-version 4b08… \
--effective-date 2026-06-01 --valid-until 2031-01-01 \
--item "Email Address:Marketing Outreach" --item 4b082452-… \
--pdf ./signed-consent.pdf --json
# -> {"consentId":"…","merkleRoot":"0x…","consentRefs":["0x…"],"assurance":"company_attested","anchored":false}The PDF is hashed locally (0x + SHA-256); only that commitment is sent. The
file bytes leave the machine only when you pass --upload. Hand the subject a
self-claim link with manual-consent claim-link --customer <id>, and revoke an
attestation with manual-consent revoke <consentRef> [--reason <text>].
Attest that a customer relationship is over (Law 25 art. 23) from your own
tooling, and undo a mistaken end within the correction window (art. 11 / art. 28).
--reason is REQUIRED and enforced before any network call (a missing reason
is exit 2, never a silent write). Scope: relationship.
agreely relationship end cust-42 --reason "account closed; purposes accomplished" --json
# -> {"customerRef":"cust-42","status":"ended","endedAt":"…","endedBy":"company"} exit 0
agreely relationship revert cust-42 --reason "offboarded the wrong account" --json
# -> {"customerRef":"cust-42","status":"active","reverted":true} exit 0Ending is a pure lifecycle overlay: it never revokes, erases, or hides any
per-cell consent. A non-undo-eligible revert is a clean 404 (exit 2).
--api-key flag (discouraged - visible in `ps`)
> AGREELY_API_KEY env (the agent path: one var, no prompt, no keychain)
> OS keychain (keytar)
> ~/.config/agreely/config.json (0600)
Base URL: --base-url > AGREELY_BASE_URL > the stored config > the SDK default
(https://api.agreely.ca). keytar is an optional native module; if it is
absent or unavailable, login / config set fall back to the 0600 config file.
The CLI is fail-closed by default. On an outage the SDK throws
AgreelyUnavailableError, which the CLI maps to exit 4 - distinct from a deny
(10) - so a caller never mistakes an outage for a refusal.
The fail-open / two-gate / break-glass degrade policy is intentionally omitted
from the CLI v1. That policy is a long-lived application/SDK-integration concern
(it carries an onDegrade audit sink and a bounded outage window), not something
a one-shot CLI invocation can persist or audit sensibly. Configure it where the
SDK is embedded.
MIT-licensed and built to be provable, not just trusted:
- No telemetry, no analytics, no phone-home. The CLI ships no trackers and
collects nothing. It makes no network calls of its own - every request goes
through
@agreely/sdk, to the Agreely API base URL you configure (defaulthttps://api.agreely.ca). - Credentials stay local. An API key comes from a flag,
AGREELY_API_KEY, the OS keychain (optionalkeytar), or a0600config file, and is sent only as theAuthorization: Bearerheader to your configured API. It is never logged (it is masked for display) and never sent anywhere else. - Minimal deps, no install scripts.
@agreely/sdk,commander,@clack/prompts,picocolors, and an optionalkeytar.
Agreely records and structures consent; it does not certify that your organization is compliant.
- Product and API: https://agreely.ca
- Organization: https://github.com/agreely-protocol
make cli-test # fast offline unit suite (mock SDK + mock prompts)
make cli-check # build + lint + unit + the live contract suite
make cli-contract # seed a fixture from the live :8081 API, then the E2E suiteThe unit suite covers the exit-code map for every outcome, pure-JSON stdout, agent-mode-never-prompts, the auth precedence, and the raw flag→SDK input mapping. The contract suite drives the built bin as a real process against the live API (allow→0, revoke→deny→10, bad key→3, issuance + idempotency replay).