Anyone can clone the repo, build it, and open a pull request — contributions and bug reports are welcome. Merges are restricted: only Reply employees can approve and merge a PR. Open one from a branch (or a fork), and a maintainer will review.
Node.js 20 or newer.
npm install
npm run build # compile TypeScript to dist/
npm test # run the test suite (vitest)
npm link # put the built `reply` binary on your PATHThe suite is fully offline — no test contacts the Reply.io API or the identity
server. fetch is stubbed, and the OAuth loopback flow is exercised against a
local 127.0.0.1 listener with an injected browser stub. CI runs the build and
tests on Linux and Windows.
npm run smoke:hosts is a separate check that runs reply skills install
entirely inside a throwaway HOME/USERPROFILE sandbox, and proves your real
home is untouched with a before/after filesystem snapshot. Native hosts
(Claude Code, Codex) are only genuinely exercised when actually installed —
each is additionally pointed at a throwaway config directory
(CLAUDE_CONFIG_DIR, CODEX_HOME) so its real plugin state is untouched
either way. Flat-directory hosts (Cursor, Gemini CLI, GitHub Copilot) are
always simulated inside the sandbox, regardless of what is really on your
machine — that's deliberate, since it's the only way the flat-directory
install path gets exercised at all. It is not part of npm test because it
clones from GitHub and, for native hosts, needs a real assistant installed to
exercise for real.
- Data is written to stdout; status and error messages go to stderr.
--jsonemits compact JSON and--prettyindented JSON. On either, an error is a single machine-readable line:{"error":{"status":…,"code":…,"title":…,"detail":…,"hint":…}}.- Exit codes:
0success,1API or runtime failure,2usage error. - Secrets are never printed; token and key fields are redacted in all output.
Credentials are stored as JSON in the config directory (~/.config/reply, or
%APPDATA%\reply on Windows), created 0600 inside a 0700 directory — the
same plaintext-file model as gh, aws, and az. On Windows the strict mode
bits are a no-op and it relies on the per-user %APPDATA% ACLs, as those tools
do.
Each record is keyed by profile name, so multiple accounts never collide even when they hit the same backend. A record is either an OAuth entry (access token
- refresh token + expiry) or an API-key entry. Expired OAuth tokens refresh
automatically; if a refresh fails the record is cleared and the user is prompted
to log in again. The store sits behind a
CredentialStoreinterface so an OS-keychain backend can be added later without touching callers.
Resolved in strict order, first hit wins:
--api-key <key>flagREPLY_API_KEYenvironment variable- the stored credential (from
auth login)
The flag and env var are ephemeral — used for the current invocation only, never
written to disk. There is no .env file lookup.
Profiles inherit the built-in prod URLs; override them to point a profile at another environment (internal testing only):
reply profile add dev \
--authority https://oauth.dev.replyapp.io \
--api-base https://api.dev.reply.io/v3
reply --profile dev auth loginAny field left off is inherited from the default (prod). Profiles live in
config.json in the config directory and can also be hand-edited:
The same source ships as two npm packages (both expose the reply bin —
identical commands and flags; only the package name and registry differ):
| Package | Registry | For | Install |
|---|---|---|---|
reply-cli |
public npm | end users | npm install -g reply-cli |
@reply-team/reply-cli |
GitHub Packages | the team, to test pre-release builds | see below |
A public X.Y.Z is byte-for-byte the internal tag vX.Y.Z — promoted, not rebuilt
differently.
To install an internal build (the newest green main, published on every
qualifying merge), point the @reply-team scope at GitHub Packages and authenticate
with a GitHub token that has read:packages:
npm config set @reply-team:registry https://npm.pkg.github.com
npm config set //npm.pkg.github.com/:_authToken <GITHUB_TOKEN> # read:packages
npm install -g @reply-team/reply-cli # newest internal build (@latest)
npm install -g @reply-team/reply-cli@0.3.0 # a specific buildreply install works on an internal build too, and keeps you on the internal
channel: it reads the package name it is running as, so it will never move you
between the two. Because the internal package lives on GitHub Packages, the
registry line and the read:packages token above have to be in place — the
command reminds you of both if the update fails. What it compares against is the
newest release of any kind, pre-releases included, which is exactly the internal
stream; the public channel compares against the promoted release instead.
Releases are automated with semantic-release.
Every merge to main runs CI. If the merged commits include a feat: or fix:
(per Conventional Commits), semantic-release:
- computes the next semver version,
- publishes it to GitHub Packages under
@latest(npm install -g @reply-team/reply-cli), - pushes a
vX.Y.Zgit tag, and - creates a pre-release GitHub Release whose notes are the changelog.
Commits that only touch docs/CI/chores (docs:, ci:, chore:, test:) do not
produce a release. A PR touching only README.md / docs/** skips the build (the
test matrix doesn't run) and never publishes; such a merge to main doesn't start
the workflow at all.
package.json's version is intentionally 0.0.0-development — the real version
of record is the git tag / GitHub Release / published package. Do not hand-edit it.
Commit messages are enforced by commitlint (a local commit-msg hook and a PR
check), because the version bump is derived from them.
To ship a tested build to the public reply-cli package on npmjs, run the
publish-public workflow (Actions → Run workflow) with a vX.Y.Z tag. It rebuilds
from that tag, publishes to public npm via OIDC trusted publishing (no token) with
provenance, and flips that tag's GitHub Release from pre-release to full/latest. The
publish is gated by the npm-public environment — a release-mergers reviewer must
approve it. A public X.Y.Z is byte-for-byte the internal tag vX.Y.Z.
reply-cli follows Semantic Versioning. The version is derived
automatically from Conventional Commit messages: fix: → patch, feat: → minor; a
major bump happens only when we explicitly declare one.
While the version is 0.x the CLI is still stabilizing, so — per semver's 0.x rule —
a minor release (0.x.0) may contain breaking changes. Pin an exact version if you
need stability before 1.0.0. We will cut 1.0.0 once the command surface and the
--json output are declared stable.
Contributor rule: do not use ! / BREAKING CHANGE: in commits while on 0.x — a
breaking change rides in a normal feat: minor. (Otherwise semantic-release would jump
straight to 1.0.0.)
From 1.0.0 onward, a major bump is required to:
- remove or rename a command, flag, or argument;
- change the meaning of an exit code;
- change the
--json/--prettyoutput shape (field names, types, structure); - change the config-file format or a
REPLY_*environment variable; - raise the minimum Node.js version.
Additive changes (a new command, a new optional flag, a new field in --json) are
minor. Fixes that don't touch the above are patch.
Before removing anything in the compatibility surface, we deprecate it first: it
keeps working and prints a warning on stderr (never stdout, so --json stays clean)
for at least one minor release. Removal happens only in a later major.