A thin command-line client over the GitVelocity MCP server. Same tools, same auth, same payloads — but driven from the shell, optimized for agents and power users.
# macOS / Linux
brew install headlinevc/tap/gitvelocity
# Verify
gv versionBinaries for darwin/arm64, darwin/amd64, linux/amd64, and windows/amd64 are published on each tag under the GitHub Releases tab.
Developers can also go install it directly — unlike the previous design, this
works with no extra setup because the CLI self-registers its OAuth client at
login time (see Authentication):
go install github.com/headlinevc/gitvelocity-cli@latest# 1. Sign in (opens browser, PKCE + loopback redirect)
gv auth login
# 2. Confirm identity
gv auth whoami --pretty
# 3. List available tools
gv tools --pretty | jq '.tools | length'
# 4. Run any tool — by name, with --json
gv add_community_org --json '{"org":"headlinevc"}' --dry-run --pretty
# 5. ...or with per-property flags
gv add_community_org --org headlinevc --dry-run --prettyThe CLI uses OAuth 2.0 authorization_code + PKCE (S256) against the GitVelocity
MCP server, with a loopback redirect on an ephemeral port. Tokens are stored
in your OS keyring (macOS Keychain / Windows Credential Manager / libsecret),
falling back to a 0600 file at $XDG_CONFIG_HOME/gitvelocity/credentials.json
when no keyring is available (headless Linux, containers, CI).
The server validates the authorize redirect_uri against the registered app's
redirect_uris with an exact-match check (no wildcards, no loopback-port
exemption). Each login binds a new ephemeral loopback port, so the
redirect_uri differs every time.
Because of that, every gv auth login registers a fresh public OAuth
client (RFC 7591 Dynamic Client Registration against the server's
registration_endpoint, discovered from
/.well-known/oauth-authorization-server) using the concrete
http://127.0.0.1:<port>/callback bound for that login — registered right
after binding the listener and before opening the browser. The minted
client_id is then persisted to $XDG_CONFIG_HOME/gitvelocity/client.json.
That cache is not reused for the next login (its redirect_uri is already
stale). Its sole consumer is the refresh-token exchange: a refresh sends
client_id but no redirect_uri, so the cached id stays valid there and must
match the app that minted the current tokens. Login overwriting client.json
keeps the tokens and the id consistent.
Tradeoff (accepted): registering fresh per login creates a new server-side OAuth app each time. It's the only scheme compatible with exact-match redirect validation under ephemeral loopback ports.
This means:
- No GitHub secret holding a
client_id, and no ldflags injection at release time.go installworks out of the box. - No admin has to pre-register the application on the server.
GV_CLIENT_ID is the explicit override: when set, login uses that
pre-registered client and skips registration. Because the server
exact-matches the callback redirect_uri, a pre-registered client REQUIRES
pinning the loopback port via GV_OAUTH_PORT, so the callback
(http://127.0.0.1:<port>/callback) matches the redirect_uri registered for
that client. Without it the check fails with invalid_redirect_uri.
GV_CLIENT_ID=my-app GV_OAUTH_PORT=53682 gv auth loginThe callback listener always binds loopback (127.0.0.1) only — there is no
bind-address knob, since binding off-loopback would expose the authorization
code to the local network during the auth window. Discovered OAuth endpoints
(authorize/token/register) must be https (loopback http is allowed for local
dev); a discovery doc advertising off-loopback http is rejected before any code
or token is sent.
| Command | Purpose |
|---|---|
gv auth login |
Browser-driven PKCE flow, self-registers client, persists tokens |
gv auth whoami |
Verify stored credentials against the server (auth-check; GitVelocity has no current-user tool) |
gv auth refresh |
Force a refresh-token exchange |
gv auth logout |
Delete stored credentials |
Optimized for agents per the agent-CLI design principles documented in
docs/agents.md:
- stdout is always JSON (the tool result envelope's first content block, pass-through verbatim).
- stderr is for human progress and hints; suppressed with
--quietor when stdout is not a TTY. - Use
--prettyto indent JSON for human reads. - Exit codes:
0success,1general failure,2usage,3not found,4permission denied,5conflict,7transport.
Tool definitions are fetched at runtime from the MCP server's tools/list
endpoint and cached under $XDG_CACHE_HOME/gitvelocity/tools-<server-url-hash>.json
for 1h. The cache key embeds a hash of the server URL, and the stored server
version is compared on each load, so both a different GV_URL and a server
version bump invalidate the cache automatically.
The cache stays current on its own — you rarely need gv tools refresh:
- After
gv auth login, the CLI fetches and caches the tool list in the same step, so a fresh login leaves you with working tool commands immediately. - On any tool-command invocation where the cache is missing or older than the 1h TTL and you have stored credentials, the CLI refreshes it (≈3s-bounded, best-effort) before dispatching — so a tool the server shipped within the last hour shows up without any manual step. If that refresh can't complete (offline, server down), it falls back silently to the existing cache and proceeds.
Static commands (--help, version, auth, tools) never trigger a startup
fetch, and nothing fetches when you're logged out.
| Command | Purpose |
|---|---|
gv tools |
List all tools (JSON) |
gv tools describe <name> |
Print one tool's schema |
gv tools refresh |
Force re-fetch from the server (manual escape hatch — usually unnecessary) |
gv <tool-name> --help |
Per-tool help, parameter table, JSON example |
| Env var | Default | Purpose |
|---|---|---|
GV_URL |
https://gitvelocity.dev |
Server base URL |
GV_CLIENT_ID |
(unset) | Use a pre-registered OAuth client_id; skips Dynamic Client Registration. Requires GV_OAUTH_PORT so the loopback redirect_uri matches what was registered. |
GV_OAUTH_PORT |
(ephemeral) | Pin the loopback callback port (for a firewall rule or a pre-registered GV_CLIENT_ID redirect_uri). The host is always 127.0.0.1. |
GV_TOKEN |
(unset) | Pre-minted MCP token for non-interactive auth (CI / GitHub Actions). When set, the browser OAuth flow and keyring are bypassed and the token is sent as the Bearer; GV_CLIENT_ID is not required. If the server rejects the token (revoked), the CLI warns; on an interactive terminal it then falls back to browser login, while in CI it fails fast with permission_denied. |
The default auth login flow opens a browser, which doesn't work in CI. For
automation, generate an MCP token in GitVelocity and pass it via the
environment — no auth login, no keyring:
GV_TOKEN=<mcp-token> gv add_community_org --org headlinevc --dry-runOn startup the CLI loads ~/.env if it exists, so you can keep GV_*
there instead of exporting from a shell profile. A real exported variable always
wins over the file, and a missing file is a no-op.
# ~/.env
GV_TOKEN=<mcp-token>Tools with side effects (prefixed create_, update_, delete_, send_,
add_, remove_, ingest_, run_, parse_, cancel_, pause_,
unpause_, save_, report_, start_) support --dry-run, which prints the
resolved payload and exits without calling the server. For GitVelocity this
covers add_community_org, start_community_import, and
start_community_bulk_import.
gv start_community_import --dry-run --json '{"org":"headlinevc"}'# Tests
go test ./...
# Local build (no client-id injection needed)
make build # produces ./gv
# Override the server URL if needed (defaults to production)
GV_URL=https://gitvelocity.dev ./gv auth loginThere is no admin-minted client_id to configure — the CLI registers its own
public OAuth client on each login (and persists the id for refresh). See
CLAUDE.md for the full architecture.
Commits on main must follow the Conventional Commits
spec. The release pipeline reads commit types to decide what to ship:
| Type | Effect |
|---|---|
feat: |
minor version bump, listed under Features in the changelog |
fix: |
patch bump, Bug Fixes |
perf: refactor: deps: |
patch bump, dedicated sections |
docs: |
visible in changelog, no bump |
test: chore: ci: build: style: |
no bump, hidden from changelog |
feat!: or BREAKING CHANGE: in body |
major bump |
Put ticket numbers in the subject as a suffix, not a prefix:
feat: add dynamic client registration (GV-394)
fix: register concrete loopback redirect_uri before authorize (GV-394)
Releases are fully automated via release-please:
- Every push to
mainupdates a long-running "release PR" with the proposed version bump and generatedCHANGELOG.md. - Merging that PR creates the
vX.Y.Ztag and a GitHub Release. - The same workflow then runs GoReleaser, publishing binaries to the
Release page and pushing the Homebrew Formula to
headlinevc/homebrew-tap.
Never hand-edit CHANGELOG.md or .release-please-manifest.json — the bot
owns both.
- Full project contract:
CLAUDE.md - Agent invariants:
docs/agents.md