# @donegate/cli

> Donegate thin client — the human leg of the gate, from your own shell.

Latest version **0.2.2** (published 2026-09-23) · UNLICENSED license · 0 weekly downloads

## Install

```sh
npm install @donegate/cli
pnpm add @donegate/cli
yarn add @donegate/cli
bun add @donegate/cli
```

## Health

**Score 55/100 (C)** — status: active.

Positive: no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types; no esm support; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.2.2 |
| Published | 2026-09-23 |
| First published | 2026-09-23 |
| Weekly downloads | 0 |
| License | UNLICENSED |
| TypeScript types | none |
| Module format | CommonJS |
| Dependencies | 0 |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | donegate |

## Links

- npm: https://www.npmjs.com/package/@donegate/cli
- Repository: https://github.com/donegate/donegate
- Homepage: https://github.com/donegate/donegate#readme
- Issues: https://github.com/donegate/donegate/issues
- npm.io page: https://npm.io/package/@donegate/cli

## Recent versions

- 0.2.2 (latest) — 2026-09-23
- 0.2.0 — 2026-09-23

## README

# donegate — the thin client

The human leg of the gate, from your own shell.

`donegate` is a **thin client**: one verb per act, one act per API call, and no
method logic on this side of the wire. It does not know what your board's
columns are, which transitions are legal, or what you should do next — it asks
the server and prints the answer. That is deliberate (canon P4), and it is why
this client keeps working when the method changes.

```
npm install -g @donegate/cli
donegate login --server https://app.donegate.io
donegate next
```

## Install

The client is published to the npm registry as **`@donegate/cli`** (CS-76,
ADR-013 addendum). Install it once, globally, and every command is `donegate
<verb>`:

```bash
npm install -g @donegate/cli
donegate whoami
```

The registry verifies integrity for you: `npm install` checks every downloaded
tarball against the `sha512` the registry holds for that exact version, and the
registry is a trust root the authority host does not control — there is no
manual checksum step, and there is no way for the host you talk to to hand you
different bytes than everyone else gets. Each published version is immutable.

**The scope is not optional.** Always `@donegate/cli`. The unscoped name on the
registry belongs to an unrelated third party; installing or running it would
run their software, not this client. A test in this repository refuses any
shipped document or message that names the bare package.

**No-install form.** `npx -y @donegate/cli <command>` runs the client without a
global install, resolving and caching it by version through the same registry
integrity check. It is what the harness wiring lines below use, because it
survives a machine where the global install was skipped.

### Wiring a harness

```bash
# Claude Code
claude mcp add donegate -- npx -y @donegate/cli mcp --harness claude-code
```

```toml
# Codex — ~/.codex/config.toml
[mcp_servers.donegate]
command = "npx"
args = ["-y", "@donegate/cli", "mcp", "--harness", "codex"]
```

With a global install, `donegate mcp --harness <label>` is the equivalent
command (`claude mcp add donegate -- donegate mcp --harness claude-code`;
Codex `command = "donegate"`, `args = ["mcp", "--harness", "codex"]`). The
`npx -y` form is the one documented because it does not depend on the
harness's `PATH` containing npm's global bin directory.

`--harness <label>` is the agent's own seat label; it never changes what the
server decides (canon P5).

### PowerShell

Install globally and call `donegate` directly — it is a `.cmd` shim on Windows
and works as-is. If you wrap the no-install form in a PowerShell function, call
`npx.cmd` rather than `npx`, or the arguments after the package name are
dropped:

```powershell
function dg { npx.cmd -y @donegate/cli @args }
```

## Getting started

```bash
donegate login --server <url> [--project <id>]   # paste your token once
donegate whoami                                  # confirm who you are
donegate next                                    # what act is available to you
```

`login` asks for your token at a **hidden prompt** — it is never a command-line
argument, because arguments land in shell history, in `ps`, and in transcripts.
Paste it once; you never handle it again. `logout` forgets it.

## Commands

Every command accepts `--json`, `--project <id>`, `--server <url>` and `--help`.

### Session

| Command | What it does |
| --- | --- |
| `join --server <url>` | Redeem an invitation. Asks for the code and the PIN at hidden prompts, mints your credential, stores it, and prints `whoami`. |
| `login --server <url> [--project <id>]` | Sign in; verifies the token before storing it. |
| `logout [--revoke]` | Forget the stored credential. With `--revoke`, revoke it on the server first. |
| `whoami` | The identity, org role and project seats behind your credential. |

`join` is the second person's first command, and the only one that sends no
credential at all: the emailed **code** and the **PIN** you were given out of
band *are* the credential. You never need a token to hand-carry, and never a
step on the server host. The seat you get is the one the invitation recorded —
`join` takes no `--project`, because a joiner does not choose the seat they were
offered.

### Inviting (org owner)

| Command | What it does |
| --- | --- |
| `invite --email <address> [--project <id> --role <architect\|developer>] [--org-role owner] [--expires <hours>]` | Issue an invitation. Prints the PIN **once**. |
| `invites` | Every invitation in this org, and where each one stands. |

The invitation is two factors on two channels. The code goes to the address by
email; the PIN is printed to you and to nobody else, and you pass it on
yourself — by voice, in person, however you like, as long as it is not the
inbox the code just landed in. Forwarding the PIN by email turns a two-factor
invitation into a one-factor one.

The PIN is never shown again, is never stored in a form anyone can read back,
and is never in the email. Five wrong attempts burn the invitation; so does
`invites` plus a revoke from the dashboard. Both surfaces are org-owner only.

### Credentials: `tokens`, `revoke`, `rotate`

| Command | What it does |
| --- | --- |
| `tokens [--scope self\|org]` | Your tokens and your agents' — id, prefix, name, holder, created, last used, expires, status. Every token now has an `expires` date; none shows `-` any more. `--scope org` lists the whole org (owner only). The one this shell is using is marked `*`. |
| `revoke <token-id>` | Revoke a token. Immediate: its next request is refused, and so is any dashboard session it signed in. |
| `rotate [<token-id>] [--print]` | Replace a token with a fresh one and revoke the old, in one server transaction. No argument rotates the token you are using and **stores the replacement for you** — nothing is printed but the identity. By id, `--print` is required: the new secret is shown once, for you to hand to the seat it belongs to — **including when the id is the token you are using**, where it is also stored for you. Either way the replacement is renamed `<old name> (rotated <UTC timestamp>)` — an existing `(rotated …)` suffix is replaced, not stacked — so two rotations of one token leave two distinct labels, unless both happen in the same second. |

Who may act on which token is decided by the server, in one place: a token's
holder, the human an agent acts for, and an org owner — nobody else, and no
hierarchy (an architect has no more say over a developer's token than a
developer has over theirs). A token id from another org is a plain 404.

`rotate` never strands you. The server mints the replacement **before** it
revokes the old token, and this shell stores the replacement **before** it
prints anything. If the store itself fails after the server has rotated, the
new secret is printed once with a warning so you can `donegate login` with it
— a credential you never saw *and* never stored is the exact outcome this verb
exists to prevent.

`rotate --harness <label>` is reserved for named agent profiles and is refused
in this version. Rotate an agent's token by id with `--print` and hand the
secret to the harness.

#### Every token expires — and `rotate` is how you renew it

Tokens have a finite lifetime (the figure and its reasoning are in **ADR-015**).
`rotate` **renews**: the replacement gets a full lifetime from the moment you
rotate, so rotating before your token runs out is the whole renewal story, and
it needs nobody's help.

You will be told before it bites. When your credential is close to its date,
every verb prints one line to **stderr**:

```
Your Donegate token expires in 9 days — run `donegate rotate` to renew it.
```

That line is on stderr, never stdout — `--json` output stays byte-for-byte the
server's response, so a script that pipes it is unaffected. `whoami` shows the
same date as an `expires` field, and the profile quietly records it beside your
credential (still nothing you ever open or edit). An ordinary command run with a
token supplied through `DONEGATE_TOKEN` never records that token's expiry there,
because it is not this profile's credential — only `login` and `rotate`, which
store a credential, write it (see [Credentials](#credentials)).

If you let it lapse, the refusal says so and points at the remedy that actually
works:

```
Your Donegate token expired on 2026-12-01. Ask an owner for a new invitation,
then run `donegate join --server <url>` — or, next time, `donegate rotate`
before it expires.
```

`donegate login` is not that remedy: it needs a token to paste, and an expired
holder has none. That is why rotating early is worth the ten seconds.

### Working a card

| Command | What it does |
| --- | --- |
| `next` | The one act available to you on this project. |
| `claim <board-item-id>` | Take responsibility for a card. |
| `release <board-item-id> [--reason <text>]` | Hand a card back. |
| `get-spec <session-id> [--version <n>]` | Read the approved spec (architects may name a version). |
| `log <session-id> --command <cmd> --exit-code <n>` | Record evidence: a command and how it exited. |
| `log <session-id> --note <text>` | Record a note. |
| `worklog <session-id>` | The session's event stream, in order. |
| `move <board-item-id> --to <status> [--reason <text>]` | Move a card. The server adjudicates the edge. |
| `ask <session-id> --question <text> (--blocking \| --non-blocking)` | Raise a question. A blocking one parks the card. |
| `answer <session-id> --seq <n> --answer <text>` | Answer an open question. |

### The human leg

| Command | What it does |
| --- | --- |
| `review <session-id> --verdict <verdict> [--note <text>]` | Record a review verdict. |
| `signoff <session-id>` | Print where to sign off. **Makes no request.** |

These are **two acts, by possibly two people**, and this client keeps them
separate on purpose. There is no `approve` that does both: the point of the gate
is that each signature is attributable, and a command that bundles them hides
the second one behind the first.

**`signoff` no longer signs off** (CS-85). A bearer token proves custody, not
presence: a process running as you reads the same credentials file you do, so a
CLI that could record a sign-off could not tell you from it (security audit
SA-01). Sign-off now needs a fresh passkey assertion, which a shell cannot
produce — so the act happens on the session page in the dashboard, and this verb
prints the address:

```
$ donegate signoff ses_01J...
sign off in the dashboard (a passkey is required): https://your-donegate/dashboard/p/prj_.../sessions/ses_01J...
```

Under `--json` it emits `{ "sign_off_url": … }`, the same key `next` carries on
the sign-off step. `--note` is gone with the wire call: the note is typed on the
page, in front of the human attesting to it.

### Ingestion (architect)

| Command | What it does |
| --- | --- |
| `session-create --title <t> --type <t> --risk <r> [--link <url>]` | Create a card and its session. |
| `spec-add <session-id> (--file <path> \| --content <text>) [--test-command <cmd>]` | Append the next spec version. |
| `spec-approve <spec-id>` | Approve a spec version. |
| `spec-list <session-id>` | List a session's spec versions (metadata only). |

## `--json`

`--json` prints **the server's response body, unaltered** — same keys, same
values, same nesting, pretty-printed. Nothing is renamed, dropped, added or
summarised, so it is safe to pipe into `jq` and safe to depend on from a script.
Human-readable output is the default and is a *rendering* of that same body;
neither is derived from the other.

## Credentials

`donegate login` stores your token for you, and `donegate logout` removes it.
That is the whole model: **you never manage a credential file.** If a command
tells you your credential is not working, the remedy is always another command —
run `donegate login` again. `donegate logout --revoke` also revokes the token on
the server before forgetting it, for a shell you are handing on or retiring;
`donegate rotate` swaps it for a fresh one without a re-login.

Your stored token is sent to exactly one place: the server you logged in to. It
is not attached to any other origin, and a redirect that leaves that origin is
refused rather than followed. Pointing a command at a different server — with
`--server` or `DONEGATE_SERVER` — does **not** carry your stored credential
there; the command refuses and tells you how to sign in to that server instead.
(A token you supply yourself in `DONEGATE_TOKEN` goes wherever you point it:
that pairing is yours to make.)

At rest the credential is written owner-only (`0600` on macOS and Linux). On
Windows the file is protected by your user profile's ACL — the same posture as
`gh` and `npm`. An OS keychain is a future hardening option, deliberately not a
dependency today: native modules are a reliability tax on a package people reach
through `npx`.

### Environment overrides

| Variable | Effect |
| --- | --- |
| `DONEGATE_TOKEN` | Use this token instead of the stored one. |
| `DONEGATE_SERVER` | Use this server instead of the stored one. |
| `DONEGATE_INVITE_CODE` | Feed `join`'s code prompt — for scripted enrolment and the test suite. |
| `DONEGATE_INVITE_PIN` | Feed `join`'s PIN prompt — same caveat. |

The two invitation variables exist so enrolment can be scripted and so the
integration suite can drive `join` with no terminal. Prefer the prompts when a
human is present: a PIN in an environment variable is a PIN in a process
listing.

Precedence is **environment > flags > stored profile**, so CI and agent callers
can act as themselves without disturbing a human's stored profile.

Pointing at a **different origin** means bringing your own token: set
`DONEGATE_TOKEN` alongside the server override. A stored credential and an
overridden server are never combined.

The expiry a command learns in passing is **never cached for a token supplied
through `DONEGATE_TOKEN`** — it is not written to the stored profile, and no
profile is created to hold it where there was none. That token is not the
profile's credential, and an ordinary command run by CI must not leave a trace in
a human's credential file. (Verbs whose whole job is to store a credential still
do: `donegate login` with `DONEGATE_TOKEN` is the non-interactive sign-in and
writes the profile, and `donegate rotate` of the token in use stores its
replacement.)

## Exit codes

| Code | Meaning |
| --- | --- |
| `0` | The act succeeded. |
| `1` | The act was refused (the server said no), or the server could not be reached. |
| `2` | The invocation was wrong — unknown command, missing argument, bad flag. |

Refusals carry their reasons. When the gate refuses a move to done, the unmet
requirements are printed in full — under `--json`, the refusal envelope comes
through untouched, so a script can branch on `details.gate.unmet` rather than
parse English.

## Requirements

Node 22 or newer (which brings `npm` and `npx`). No runtime dependencies.

---
_Source: https://npm.io/package/@donegate/cli · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
