grm CLI — Auth MVP Design
Design for the grm CLI's authentication commands (login, logout, whoami) using OAuth 2.0 Device Authorization Flow with Better Auth's deviceAuthorization and apiKey plugins. The CLI stores a long-lived API key locally after browser-based sign-in.
- Status Approved
A Cobra-based Go CLI (grm) that authenticates via OAuth 2.0 Device Authorization Flow, stores a long-lived API key locally, and exposes auth login, auth logout, and auth whoami commands for both humans and AI agents.
Goals
- Developer experience
- Replace interactive shell scripts with a single typed command
- Agent interface
- AI agents authenticate once and use the stored API key for all subsequent operations without human involvement
- Binary name
grm(changeable via build flag)- Scope
- MVP — auth commands only
Key Decisions
Authentication method
OAuth 2.0 Device Authorization Flow via Better Auth's deviceAuthorization plugin.
Works for both interactive humans (browser opens automatically) and headless AI agents (paste the code). No credentials ever pass through the terminal.
Rejected: --email --password flag-based login (not needed given device flow).
Credential storage
Long-lived API key written to ~/.config/grm/config.json after device flow completes.
Agents authenticate once and reuse the key indefinitely. Simple file-based config avoids OS keychain complexity for MVP.
Rejected: session tokens (expire too quickly for agents), OS keychain (cross-platform complexity).
Cobra abstraction layer
Thin declarative layer: CommandDef, FlagDef, Context, Build().
Raw Cobra is verbose — scattered init(), repeated flag wiring. The abstraction makes each command a single struct literal. --json is free on every command. Actions are plain functions, easy to unit test.
Rejected: raw Cobra (too much boilerplate), alternative CLI frameworks (lose Cobra completions/man pages).
Architecture
Components
- Bun backend
- Add
deviceAuthorization+apiKeyplugins to Better Auth config; add/deviceverification page - CLI source
backend/go/cmd/cli/- Config file
~/.config/grm/config.json
Directory Structure
cmd/cli/
main.go # entrypoint — calls cli.Build([]cli.CommandDef{...}).Execute()
cmd/
auth/
login.go # cli.CommandDef for `grm auth login`
logout.go # cli.CommandDef for `grm auth logout`
whoami.go # cli.CommandDef for `grm auth whoami`
internal/
config/ # read/write ~/.config/grm/config.json
client/ # HTTP client wrapping Bun API calls
pkg/
cli/
builder.go # CommandDef, FlagDef, Context, Build(), wireFlags()
Config File
{
"base_url": "https://api.yourdomain.com",
"api_key": "ak_..."
}
base_url must be set on first run. If absent, grm auth login prompts: "Enter your API base URL:" and saves it to config before proceeding.
Auth Flow
grm auth login (Device Authorization Flow)
POST /api/auth/device/codewith{client_id: "grm-cli"}- Receive
device_code,user_code,verification_uri,interval - Open browser to
verification_uri - Poll
POST /api/auth/device/tokeneveryintervalseconds - Receive
access_tokenon user approval POST /api/auth/api-key/createwith{name: "grm-cli"}using Bearer token- Write
api_key+base_urlto config - Print "Logged in as wayne@example.com"
grm auth logout
- Deletes
api_keyfrom config file - Prints "Logged out"
grm auth whoami
- Reads
api_keyfrom config GET /api/auth/mewithx-api-key: ak_...header- Prints "Logged in as wayne@example.com"
Commands
| Command | Purpose |
|---|---|
grm auth login | Device flow → store API key |
grm auth logout | Delete API key from config |
grm auth whoami | Show current authenticated user |
Global Flags
--config- Path to config file (default:
~/.config/grm/config.json) --base-url- Override API base URL (default from config)
--json- Machine-readable JSON output (for agent use)
Error Handling
| Condition | Message |
|---|---|
| No config / not logged in | Not logged in. Run: grm auth login |
| Device code expired | Login timed out. Run: grm auth login to try again |
| API key invalid or revoked | Session expired. Run: grm auth login |
| Network error | Request to <url> failed: <error> |
Testing
- Unit tests for
internal/configpackage: read, write, delete config file - Integration tests for auth commands using
httptestserver mocking Bun endpoints - No real network calls in tests
Dependencies
| Concern | Library |
|---|---|
| CLI framework | github.com/spf13/cobra |
| Config file | standard encoding/json |
| Open browser | github.com/pkg/browser |
| HTTP client | standard net/http |
Out of Scope (MVP)
grm dev— start services (future)grm db migrate— database commands (future)- Agent Auth plugin (OAuth 2.1 agent protocol) — future
grm auth login --email --passwordflag-based login — not needed given device flow
Open Questions
Variants 1
Alternate renderings of the same source markdown. The canonical version above is what powers the archive and search; variants are kept side-by-side for comparison.
codex
Summary
The MVP gives Gremlin a typed grm CLI for authentication only: login, logout, and whoami. It replaces interactive shell scripts and gives AI agents a one-time browser-assisted login that becomes a stored API key for later non-interactive use.
Decisions
Use device flow, then mint an API key
grm auth login starts Better Auth device authorization, opens the browser verification URI, polls for approval, then calls /api/auth/api-key/create.
This keeps login browser-based for humans while producing a long-lived ak_... credential that agents can reuse without human involvement.
Rejected for MVP: grm auth login --email --password; device flow is enough and avoids credential collection in the CLI.
Wrap Cobra with a thin declarative layer
Commands are declared as CommandDef structs with FlagDef entries and an Action func(*Context) error.
Raw Cobra would scatter init(), AddCommand, and repeated --json wiring across files. The wrapper makes each command a small struct literal while preserving Cobra completions and man pages.
Rejected: direct Cobra wiring in every command file.
Store local auth state in one JSON config file
~/.config/grm/config.json stores base_url and api_key; --config and --base-url can override defaults.
A plain JSON file is easy to inspect, test, and delete. Prompting for base_url on first login removes a required upfront setup step.
Not specified for MVP: keychain storage, multi-profile config, or encrypted local state.
Architecture
Bun backend changes
Better Auth- Add
deviceAuthorizationplugin. - Add
apiKeyplugin for long-lived API key generation. - Add a
/devicebrowser verification page.
CLI source
Go + Cobrabackend/go/cmd/cli/contains the binary entrypoint and auth commands.internal/configowns config read/write/delete.internal/clientwraps Bun API calls.
Command builder
pkg/cli
CommandDef,FlagDef,Context, andBuild().ctx.Output(v any)selects JSON or pretty output.--jsonis attached once as a persistent flag.
cmd/cli/
main.go # entrypoint - calls cli.Build([]cli.CommandDef{...}).Execute()
cmd/
auth/
login.go # cli.CommandDef for `grm auth login`
logout.go # cli.CommandDef for `grm auth logout`
whoami.go # cli.CommandDef for `grm auth whoami`
internal/
config/ # read/write ~/.config/grm/config.json
client/ # HTTP client wrapping Bun API calls
pkg/
cli/
builder.go # CommandDef, FlagDef, Context, Build(), wireFlags()
Auth Flow
-
Start login
grm auth loginreads config. Ifbase_urlis absent, it promptsEnter your API base URL:and saves the value before continuing. -
Request a device code
POST
/api/auth/device/codewith{client_id: "grm-cli"}; receivedevice_code,user_code,verification_uri, andinterval. -
Open browser verification
Open
verification_uriso the user can sign in and approve the code in the browser. -
Poll for approval
POST
/api/auth/device/tokeneveryintervalseconds until approval returns anaccess_tokenor the device code expires. -
Create and store API key
POST
/api/auth/api-key/createwith the bearer token, receive{key: "ak_..."}, then writeapi_keyandbase_urlinto~/.config/grm/config.json. -
Confirm identity
Print
Logged in as wayne@example.com. Later,grm auth whoamisendsx-api-key: ak_...toGET /api/auth/me.
{
"base_url": "https://api.yourdomain.com",
"api_key": "ak_..."
}
Commands And Output
| Command | Behavior | Human output |
|---|---|---|
grm auth login |
Runs device flow, creates API key, stores config. | Logged in as wayne@example.com |
grm auth logout |
Deletes api_key from ~/.config/grm/config.json. |
Logged out |
grm auth whoami |
Reads api_key and calls GET /api/auth/me. |
Logged in as wayne@example.com |
--config- Path to config file, defaulting to
~/.config/grm/config.json. --base-url- Overrides the API base URL, defaulting to the value from config.
--json- Machine-readable JSON output for agent use; available on auth commands through the shared command context.
Errors And Testing
| Condition | Message |
|---|---|
| No config or not logged in | Not logged in. Run: grm auth login |
| Device code expired | Login timed out. Run: grm auth login to try again |
| API key invalid or revoked | Session expired. Run: grm auth login |
| Network error | Request to <url> failed: <error> |
- Unit tests for
internal/config: read, write, and delete config file. - Integration tests for auth commands using mocked device-code, token, API-key, and
/api/auth/meresponses.
Scope And Dependencies
In MVP
auth onlygrm auth logingrm auth logoutgrm auth whoami
Out of scope
futuregrm devfor starting services.grm db migratefor database commands.- Agent Auth plugin / OAuth 2.1 agent protocol.
grm auth login --email --password.
| Concern | Library |
|---|---|
| CLI framework | github.com/spf13/cobra |
| Config file | standard encoding/json |
| Open browser | github.com/pkg/browser |
| HTTP client | standard net/http |