Skip to content

headlinevc/gitvelocity-cli

Repository files navigation

GitVelocity CLI

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.

Install

# macOS / Linux
brew install headlinevc/tap/gitvelocity

# Verify
gv version

Binaries 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

Quickstart

# 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 --pretty

Authentication

The 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).

Dynamic Client Registration (no admin-minted client_id)

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 install works out of the box.
  • No admin has to pre-register the application on the server.

Pre-registered client (GV_CLIENT_ID)

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 login

The 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

Output contract

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 --quiet or when stdout is not a TTY.
  • Use --pretty to indent JSON for human reads.
  • Exit codes: 0 success, 1 general failure, 2 usage, 3 not found, 4 permission denied, 5 conflict, 7 transport.

Tool discovery

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

Configuration

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.

Non-interactive auth (CI)

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-run

Config via ~/.env

On 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>

Mutating tools

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"}'

Development

# 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 login

There 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.

Contributing & Releases

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:

  1. Every push to main updates a long-running "release PR" with the proposed version bump and generated CHANGELOG.md.
  2. Merging that PR creates the vX.Y.Z tag and a GitHub Release.
  3. 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.

Related

  • Full project contract: CLAUDE.md
  • Agent invariants: docs/agents.md

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages