Skip to main content

Gravi CLI

Command-line tool for Gravitate infrastructure management. Provides CLI access to mom infrastructure, allowing developers to authenticate, manage tokens, and access instance configurations and credentials.

For agents: there's a sister command, gravi-axi — an AXI surface over the same burner tooling with TOON output, pre-computed aggregates, structured errors, and one-call up/wait. Ships from this same package. See AXI.md.

Features

  • OAuth-style device authorization - Secure browser-based authentication
  • Automatic token refresh - Tokens auto-renew when <7 days remaining
  • Token management - List, revoke, and manage CLI tokens
  • Instance access - Get configurations and credentials for Gravitate instances
  • Python library API - Use programmatically in scripts and applications
  • CI/CD support - Environment variable-based authentication

Installation

From Source (Development)

Using uv (recommended):

cd /home/jvogel/src/work/tools/mom/cli
uv pip install -e ".[test]"

Or with traditional pip:

cd /home/jvogel/src/work/tools/mom/cli
pip install -e ".[test]"

From PyPI (Future)

pip install gravi-cli
# or
uv pip install gravi-cli

Quick Start

Note: If you installed with uv pip install -e ., you can run the CLI using uvx --from . gravi or just gravi if it's in your PATH.

1. Login

gravi login
# or with uvx:
uvx --from . gravi login

This will:

  1. Open your browser to mom authorization page
  2. Display a user code (e.g., ABCD-1234)
  3. Wait for you to authorize in the browser
  4. Save credentials to ~/.config/gravi/config.json

2. Check Status

gravi status

Shows:

  • Current user email
  • Mom URL
  • Device name
  • Token ID
  • Token expiry

3. Get Instance Configuration

# Get config as JSON
gravi config dev

# Get config as environment variables
gravi config prod --format=env

4. Logout

gravi logout

Revokes token on server and clears local credentials.

CLI Commands

Authentication

gravi login                          # Authenticate via browser
gravi logout                         # Clear credentials and revoke token
gravi status                         # Show login status and token expiry
gravi whoami                         # Show current user info

Token Management

gravi tokens list                    # List all CLI tokens
gravi tokens revoke <token_id>       # Revoke a specific token
gravi tokens revoke --all            # Revoke all tokens

Instance Access

gravi config <instance_key>          # Get instance configuration
gravi config <instance_key> --format=env    # Get as environment variables

Seed Datasets

A dataset version is the whole payload, not a patch — publishing one that omits a tab publishes a dataset without that tab, and the burner it seeds still boots ready. So push sends everything and refuses a payload missing tanks, supply or drivers; --merge layers the tabs you edited onto the latest version instead.

gravi dataset list [--q costco] [--tag e2e]
gravi dataset show <name> [--version 3]      # counts, services, version history
gravi dataset diff <name> [--against 8]      # row-level, per tab

gravi dataset clone <name> <new-name>        # never edit a shared dataset in place
gravi dataset pull <name> [-o dir]           # one JSON file per tab, sorted keys
$EDITOR <name>/drivers.json
gravi dataset push <name> --notes "why"      # append a new version
gravi dataset push <name> --merge            # only the tabs present on disk
gravi dataset push <name> --dry-run          # report the shape, send nothing

gravi dataset sheet export <name> --open     # edit in Sheets instead
gravi dataset sheet import <name> <sheet-id>

pull writes stable, sorted JSON with one file per tab, so a dataset can live in a repo and a rename across it reviews as an ordinary diff. Non-row keys (title, service_config, seeding_config, env_overrides) land in _meta.json, and push reassembles them — the round trip is lossless.

Demand Model Debugger

Everything mom's Demand Model Debugger page can do, minus its two LLM tabs. The loop the page is built around is three commands — find the parent run, find the child that failed, pull its solver payload:

gravi demand triage --range 7d               # failed runs, every customer
gravi demand runs <instance> --failed        # one customer's parent runs
gravi demand children <instance> <history_id> --incomplete
gravi demand pull <instance> <run_id> --summary      # status/orders/cost chips
gravi demand pull <instance> <run_id> --request-only -o req.json

When you already know the store and the day — the usual way the question arrives — go straight there instead:

gravi demand store marathon "ANB 881" --date 2026-09-10
#   → the child run_ids covering that store that day, newest first

That's one API call: mom's /recent_runs already returns each parent's full store → child run_id table, so nothing has to walk run_detail per parent (77KB each, ~17 parents on a busy day). A store number matches exactly when anything does and as a substring otherwise, so "881" finds ANB 881 — the store column shows what else it caught.

store and runs both take --date YYYY-MM-DD, or --since/--until for a range. --days widens automatically to reach whatever day you asked for.

Day boundaries are UTC, matching time_ran. A US customer's late-evening run carries the next UTC date, so a single --date can miss it — widen the window with --since/--until when a result looks a run or two short.

Then edit req.json and re-run it against the customer's own solver:

$EDITOR req.json
gravi demand rerun <instance> <run_id> -f req.json

...or make the edit inline. --set addresses a field by name inside the request's two nested lists, because nobody knows a tank's ordinal:

gravi demand rerun <instance> <run_id> \
  --set 'stores[GOM0019].tanks[2].currentInventory=4000' \
  --set 'stores[*].tanks[*].erDrainCost=0' \
  --set 'multiStoreDropCost=250'

A bracket selector matches a name before an index. It first looks for an element whose tank / name / store field equals it, and only falls back to positional addressing when nothing goes by that name. Real tank ids are plain digit strings — tank "1" sits at index 0 — so a positional reading of tanks[1] would quietly edit the wrong tank. [*] fans out across the whole list, and [0] / [-1] still index a list whose elements aren't named.

Field names work in either case convention. GCS stores the request snake_case while mom's schema and the UI talk camelCase, so --set multiProductDropCost=500 and --set multi_product_drop_cost=500 both hit the key that's really there rather than adding a second one beside it.

Values parse as JSON when they can (0, null, [1,2]), otherwise as a bare string. pull --request-only writes the unwrapped ModelRunRequest these paths address — GCS stores it inside a DemandModelStartReq envelope, which pull and rerun -f both peel for you.

rerun is ephemeral. Mom forwards the edited request straight to the customer's solver and hands the answer back: nothing is written to the customer DB or to GCS, and the original child run stays the source of truth. By default it prints a before/after comparison against that original run, and --dry-run shows the edits without spending solver time.

The rest of the page:

gravi demand store <instance> <store> [--date D | --since D --until D]
gravi demand dash [--range 24h] [--env prod] [--attention] [--refresh]
gravi demand adoption [--stage active]
gravi demand show <instance> <history_id> [--logs]
gravi demand orders <instance> <run_id> [--audits]   # did they reach SND?
gravi demand annotate <instance> <history_id> --state resolved --note "why"
gravi demand annotations <instance>
gravi demand solver show <instance>
gravi demand solver set <instance> prod              # prompts; --yes to skip

dash, adoption and triage read a Redis blob an arq cron refreshes every 6h. On a cold cache mom enqueues a refresh and answers "warming" — re-run in ~30s rather than reading that as "no runs".

Annotations are shared: marking a run resolved here is the same act as marking it resolved in the UI, and everyone sees it.

What writes. Everything above reads, with three exceptions: annotate / unannotate write to mom (team-visible), --refresh enqueues a mom-side cache job, and solver set writes to the customer's own demand_model_config through their API — changing which solver their production runs use. rerun writes nothing anywhere; it forwards one request to the solver and returns the answer. solver set confirms in both directions (--yes to skip); the agent surface requires an explicit --confirm flag and refuses with exit 2 without it.

For driving this from an agent, use gravi-axi demand — same surface, TOON output, structured errors. See AXI.md.

Python Library API

Use gravi_cli programmatically in Python scripts:

from gravi_cli.api import get_instance_config, get_instance_token

# Get database credentials
config = get_instance_config("prod")
db_url = config["config"]["database_url"]

# Get ServiceNow access token
token_response = get_instance_token("dev")
sn_token = token_response["access_token"]

# Get mom access token directly
from gravi_cli.api import get_mom_token
mom_token = get_mom_token()  # Auto-refreshes if needed

Example: Database Connection

from gravi_cli.api import get_instance_config
import psycopg2

# Get prod database credentials
config = get_instance_config("prod")
conn = psycopg2.connect(config["config"]["database_url"])

# Use database
cursor = conn.cursor()
cursor.execute("SELECT * FROM users LIMIT 10")

Example: ServiceNow API

from gravi_cli.api import get_instance_config, get_instance_token
import requests

# Get ServiceNow config and token
config = get_instance_config("dev")
token_response = get_instance_token("dev")

# Make ServiceNow API call
response = requests.get(
    f"{config['api_url']}/api/now/table/incident",
    headers={"Authorization": f"Bearer {token_response['access_token']}"}
)
incidents = response.json()

CI/CD Usage

For automated scripts and CI/CD pipelines:

# Set refresh token as environment variable
export GRAVI_REFRESH_TOKEN="your_refresh_token_here"

# Commands will automatically use the env var
gravi config prod

Getting a CI/CD token:

  1. Run gravi login on your local machine
  2. Run gravi tokens list to see your token ID
  3. Copy the refresh token from ~/.config/gravi/config.json
  4. Set as GRAVI_REFRESH_TOKEN in your CI/CD system

Security Note: Treat GRAVI_REFRESH_TOKEN like a password. Use secret management systems (GitHub Secrets, AWS Secrets Manager, etc.) to store it securely.

Configuration

Config File Location

~/.config/gravi/config.json

Config File Format

{
  "version": 1,
  "user_email": "john.doe@gravitate.com",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token_expires_at": "2025-11-09T10:30:00Z",
  "token_id": "507f1f77bcf86cd799439011",
  "device_name": "John's MacBook"
}

Environment Variables

  • GRAVI_MOM_URL - Override mom API URL (default: https://mom.gravitate.energy)
  • GRAVI_REFRESH_TOKEN - CI/CD refresh token (bypasses config file)

File Permissions

Config file is automatically set to 0600 (owner read/write only) for security.

Token Auto-Renewal

Refresh tokens are automatically renewed when <7 days remaining:

  • CLI checks expiry on each use
  • If <7 days: requests new 14-day token
  • Config file updated automatically
  • Seamless for users - no re-login needed

Development

Setup

cd /home/jvogel/src/work/tools/mom/cli
pip install -e ".[test]"

Run Tests

# All tests
pytest

# With coverage
pytest --cov=gravi_cli --cov-report=term-missing

# Specific test file
pytest tests/test_config.py

# Verbose
pytest -v

Project Structure

cli/
├── gravi_cli/
│   ├── __init__.py          # Package metadata
│   ├── api.py               # Public Python API
│   ├── auth.py              # Token refresh and auth logic
│   ├── cli.py               # CLI commands (Click)
│   ├── client.py            # Mom API client
│   ├── config.py            # Config file management
│   └── exceptions.py        # Custom exceptions
├── tests/
│   ├── conftest.py          # Pytest fixtures
│   ├── test_config.py       # Config tests
│   └── test_client.py       # API client tests
├── pyproject.toml           # Package configuration
└── README.md                # This file

Troubleshooting

"Not logged in" Error

gravi login

"Token expired" Error

Tokens auto-renew, but if expired:

gravi login

Rate Limiting

If you hit rate limits, wait and retry. Rate limits reset every minute.

Mom URL Override (Development)

# Temporary override
GRAVI_MOM_URL=https://mom.dev gravi login

# Or with flag
gravi login --mom-url=https://mom.dev

Security

  • Refresh tokens stored in config file with 0600 permissions
  • Access tokens never persisted (in-memory only)
  • No tokens in CLI arguments (prevents shell history exposure)
  • HTTPS only for all API calls
  • Audit logging - All token operations logged in mom

Support

For issues or questions:

License

MIT

Metadata

Release files for gravi-cli 0.18.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for gravi-cli 0.18.0
File Interpreter ABI Platform
gravi_cli-0.18.0-py3-none-any.whl Python 3 none any Details

Release files / gravi_cli-0.18.0-py3-none-any.whl

Download URL gravi_cli-0.18.0-py3-none-any.whl
Size 119.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
03391c931c5bdc07a1f5b958f2d84f51790754396c8f894674e5edd000d497fe
BLAKE2b-256 checksum
How to use checksums
a2ef05694043e1f6cfaff0c3e2d61ecfc5582315badedc025fac08ddd817b432
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release history Release notifications | RSS feed

0.32.8

1 release file

0.32.7

1 release file

0.32.6

1 release file

0.32.5

1 release file

0.32.4

1 release file

0.32.3

1 release file

0.32.2

1 release file

0.32.1

1 release file

0.32.0

1 release file

0.31.3

1 release file

0.31.2

1 release file

0.31.0

1 release file

0.30.1

1 release file

0.30.0

1 release file

0.29.0

1 release file

0.28.0

1 release file

0.27.0

1 release file

0.26.0

1 release file

0.25.0

1 release file

0.24.0

1 release file

0.23.0

1 release file

0.22.1

1 release file

0.22.0

1 release file

0.21.1

1 release file

0.21.0

1 release file

0.20.0

1 release file

0.19.1

1 release file

0.19.0

1 release file

This release

0.18.0 This release

1 release file

0.17.0

1 release file

0.16.0

1 release file

0.15.3

1 release file

0.15.1

1 release file

0.15.0

1 release file

0.14.0

1 release file

0.13.20

1 release file

0.13.19

1 release file

0.13.18

1 release file

0.13.17

1 release file

0.13.16

1 release file

0.13.15

1 release file

0.13.14

1 release file

0.13.13

1 release file

0.13.12

1 release file

0.13.11

1 release file

0.13.10

1 release file

0.13.9

1 release file

0.13.8

1 release file

0.13.7

1 release file

0.13.6

1 release file

0.13.5

1 release file

0.13.4

1 release file

0.13.3

1 release file

0.13.2

1 release file

0.13.1

1 release file

0.13.0

2 release files

0.12.2

2 release files

0.12.1

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.1

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page