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

View source markdown ↗ generated by claude · diagrams mermaid

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 + apiKey plugins to Better Auth config; add /device verification 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.

System components: CLI communicates with Better Auth plugins on Bun backend, stores credentials locally.
```_mermaid flowchart TB subgraph "Bun Backend" BA[Better Auth] DA[deviceAuthorization plugin] AK[apiKey plugin] VP["/device verification page"] end subgraph cli_go ["CLI (Go)"] CMD["grm auth login/logout/whoami"] CFG["~/.config/grm/config.json"] PKG["pkg/cli/builder.go"] end CMD -->|"POST /api/auth/device/code"| DA CMD -->|"POST /api/auth/api-key/create"| AK CMD -->|"GET /api/auth/me"| BA CMD --> CFG PKG -->|wires| CMD DA --> VP ```
System components: CLI communicates with Better Auth plugins on Bun backend, stores credentials locally.

Auth Flow

grm auth login (Device Authorization Flow)

  1. POST /api/auth/device/code with {client_id: "grm-cli"}
  2. Receive device_code, user_code, verification_uri, interval
  3. Open browser to verification_uri
  4. Poll POST /api/auth/device/token every interval seconds
  5. Receive access_token on user approval
  6. POST /api/auth/api-key/create with {name: "grm-cli"} using Bearer token
  7. Write api_key + base_url to config
  8. Print "Logged in as wayne@example.com"
Device Authorization Flow sequence: CLI initiates, user approves in browser, CLI exchanges for API key.
```_mermaid sequenceDiagram participant User participant CLI as grm CLI participant API as Bun Backend participant Browser CLI->>API: POST /api/auth/device/code API-->>CLI: device_code, user_code, verification_uri CLI->>Browser: Open verification_uri User->>Browser: Enter user_code & approve loop Poll every interval seconds CLI->>API: POST /api/auth/device/token API-->>CLI: pending / access_token end CLI->>API: POST /api/auth/api-key/create (Bearer token) API-->>CLI: api_key (ak_...) CLI->>CLI: Write config.json ```
Device Authorization Flow sequence: CLI initiates, user approves in browser, CLI exchanges for API key.

grm auth logout

  • Deletes api_key from config file
  • Prints "Logged out"

grm auth whoami

  • Reads api_key from config
  • GET /api/auth/me with x-api-key: ak_... header
  • Prints "Logged in as wayne@example.com"

Commands

CommandPurpose
grm auth loginDevice flow → store API key
grm auth logoutDelete API key from config
grm auth whoamiShow 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

ConditionMessage
No config / not logged inNot logged in. Run: grm auth login
Device code expiredLogin timed out. Run: grm auth login to try again
API key invalid or revokedSession expired. Run: grm auth login
Network errorRequest to <url> failed: <error>

Testing

  • Unit tests for internal/config package: read, write, delete config file
  • Integration tests for auth commands using httptest server mocking Bun endpoints
  • No real network calls in tests

Dependencies

ConcernLibrary
CLI frameworkgithub.com/spf13/cobra
Config filestandard encoding/json
Open browsergithub.com/pkg/browser
HTTP clientstandard 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 --password flag-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 generated by 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.

  • Scope MVP auth commands only
  • Binary grm, changeable via build flag
  • Source backend/go/cmd/cli/
  • Auth Better Auth device flow + API key
  • Config ~/.config/grm/config.json
  • Primary users Developers and AI agents

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

grm CLI auth architecture. The CLI sends device-code and token requests to the Better Auth API, opens a browser verification page for the human approval step, and writes the resulting base_url and api_key into the local config file. The diagram also shows the Cobra command builder feeding the auth command surface and the httptest boundary used for integration tests.
Auth MVP components and flow boundaries.

Bun backend changes

Better Auth
  • Add deviceAuthorization plugin.
  • Add apiKey plugin for long-lived API key generation.
  • Add a /device browser verification page.

CLI source

Go + Cobra
  • backend/go/cmd/cli/ contains the binary entrypoint and auth commands.
  • internal/config owns config read/write/delete.
  • internal/client wraps Bun API calls.

Command builder

pkg/cli
  • CommandDef, FlagDef, Context, and Build().
  • ctx.Output(v any) selects JSON or pretty output.
  • --json is 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

  1. Start login

    grm auth login reads config. If base_url is absent, it prompts Enter your API base URL: and saves the value before continuing.

  2. Request a device code

    POST /api/auth/device/code with {client_id: "grm-cli"}; receive device_code, user_code, verification_uri, and interval.

  3. Open browser verification

    Open verification_uri so the user can sign in and approve the code in the browser.

  4. Poll for approval

    POST /api/auth/device/token every interval seconds until approval returns an access_token or the device code expires.

  5. Create and store API key

    POST /api/auth/api-key/create with the bearer token, receive {key: "ak_..."}, then write api_key and base_url into ~/.config/grm/config.json.

  6. Confirm identity

    Print Logged in as wayne@example.com. Later, grm auth whoami sends x-api-key: ak_... to GET /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/me responses.

Scope And Dependencies

In MVP

auth only
  • grm auth login
  • grm auth logout
  • grm auth whoami

Out of scope

future
  • grm dev for starting services.
  • grm db migrate for 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