---
import BaseLayout from '../layouts/BaseLayout.astro';
import ArchitectureCards from '../components/ArchitectureCards.astro';
import ApiSurface from '../components/ApiSurface.astro';
import CodeBlock from '../components/CodeBlock.astro';
import CommandInventory from '../components/CommandInventory.astro';
import FeatureRow from '../components/FeatureRow.astro';
import InlineCode from '../components/InlineCode.astro';
import InstallMarquee from '../components/InstallMarquee.astro';
import Mascot from '../components/Mascot.astro';
import SectionIntro from '../components/SectionIntro.astro';
import DemoPlayer from '../components/DemoPlayer.astro';
import Stats from '../components/Stats.astro';
import { demoDurationSeconds } from '../lib/demo-duration';
import heroTerminal from '../assets/hero-terminal.svg?raw';
import { discoveryMethodTotal, discoveryServiceCount, discoveryServices } from '../data/discovery-coverage';
import artMail from '../assets/art-mail.svg';
import artCalendar from '../assets/art-calendar.svg';
import artDrive from '../assets/art-drive.svg';
import artContacts from '../assets/art-contacts.svg';
import artChat from '../assets/art-chat.svg';
import artForms from '../assets/art-forms.svg';
import glyphMail from '../assets/glyph-mail.svg';
import glyphCalendar from '../assets/glyph-calendar.svg';
import glyphContacts from '../assets/glyph-contacts.svg';
import glyphChat from '../assets/glyph-chat.svg';
// The per-service line under each card in the inventory panel; the count is
// read from the coverage table rather than typed, so it cannot drift.
const methodCountFor = (name: string): number =>
discoveryServices.find((service) => service.name === name)?.methods ?? 0;
const base = import.meta.env.BASE_URL;
const releaseUrl = 'https://github.com/debanjanbasu/grr-cli/releases';
const services = [
{
name: 'Gmail',
eyebrow: '01 / inbox',
art: artMail,
accent: '#f97316',
description: 'Search, read, compose, label, filter, route mail, and batch without leaving your shell.',
subfeatures: ['Search and read messages', 'Compose, label, and route mail', 'Batch and filter messages', 'Manage threads, history, and send-as'],
command: 'grr gmail users messages list --user-id me --q "in:inbox" --max-results 5',
commands: [
'grr gmail users messages list --user-id me --q "in:inbox" --max-results 5',
'grr gmail users settings filters list --user-id me',
'grr gmail users threads list --user-id me --max-results 10',
'grr gmail users settings sendAs verify --user-id me --send-as-email alias@example.com',
],
},
{
name: 'Calendar',
eyebrow: '02 / time',
art: artCalendar,
accent: '#fb923c',
description: 'List calendars, manage events, watch changes, and check free-busy windows in one pass.',
subfeatures: ['List calendars and events', 'Create, update, move, and delete', 'Watch changes and free-busy', 'Manage sharing, settings, and colors'],
command: 'grr calendar events list --calendar-id primary --max-results 10',
commands: [
'grr calendar events list --calendar-id primary --max-results 10',
'grr calendar events patch --calendar-id primary --event-id <event-id> --params \'{"summary":"Updated"}\'',
'grr calendar events watch --calendar-id primary --params \'{"type":"web_hook","address":"https://example.com/hook"}\'',
],
},
{
name: 'Drive',
eyebrow: '03 / files',
art: artDrive,
accent: '#ea580c',
description: 'Find files, inspect metadata, manage sharing and comments, and control storage.',
subfeatures: ['Find files and inspect metadata', 'Upload, download, and export', 'Manage sharing and comments', 'Restore, rename, and control storage'],
command: 'grr drive files list --q "trashed = true" --page-size 20',
commands: [
'grr drive files list --q "trashed = true" --page-size 20',
'grr drive comments create --file-id <file-id> --params \'{"content":"Note"}\'',
'grr drive about get',
],
},
{
name: 'Contacts',
eyebrow: '04 / people',
art: artContacts,
accent: '#c2410c',
description: 'Query people, organize groups, batch-read profiles, and keep photos close to your workflows.',
subfeatures: ['Search and organize people', 'Create, update, and manage groups', 'Batch-read profiles and photos', 'Adopt contacts and keep photos close'],
command: 'grr people people connections list --resource-name people/me --page-size 20',
commands: [
'grr people people connections list --resource-name people/me --page-size 20',
'grr people people getBatchGet --resource-names people/123',
'grr people people updateContactPhoto --resource-name people/123 --body-file photo.json',
],
},
{
name: 'Chat',
eyebrow: '05 / rooms',
art: artChat,
accent: '#fdba74',
description: 'Browse spaces, manage members, read messages, and react without context switching.',
subfeatures: ['Browse spaces and members', 'Add and remove members', 'Read and send messages', 'React and unreact to messages'],
command: 'grr chat spaces list --page-size 10',
commands: [
'grr chat spaces list --page-size 10',
'grr chat spaces members create --parent spaces/<space-id> --params \'{"member":{"name":"users/user@example.com"}}\'',
'grr chat spaces messages reactions create --parent spaces/<space-id>/messages/<message-id> --params \'{"emoji":"👍"}\'',
],
},
{
name: 'Forms',
eyebrow: '06 / signals',
art: artForms,
accent: '#c2410c',
description: 'Read and update forms, pull responses, and manage watch channels for lightweight automation.',
subfeatures: ['Read and update forms', 'Pull response data', 'Create and manage watch channels', 'Renew and delete watches'],
command: 'grr forms forms get --form-id <form-id>',
commands: [
'grr forms forms get --form-id <form-id>',
'grr forms forms responses list --form-id <form-id> --page-size 50',
'grr forms forms watches renew --form-id <form-id> --watch-id <watch-id>',
],
},
];
const commandSurfaces = [
{
name: 'Gmail',
commands: 'users: messages, threads, drafts, labels, history, settings (filters, forwarding, sendAs, CSE), watch · 79 methods',
},
{
name: 'Calendar',
commands: 'calendars, events, calendarList, freebusy, acl, colors, settings, channels · 38 methods',
},
{
name: 'Drive',
commands: 'files, permissions, comments, replies, revisions, changes, drives, accessproposals, approvals, teamdrives · 64 methods',
},
{
name: 'People',
commands: 'people, contactGroups, otherContacts · 24 methods',
},
{
name: 'Chat',
commands: 'spaces, messages, members, reactions, spaceEvents, customEmojis, media, availability · 54 methods',
},
{
name: 'Forms',
commands: 'forms, responses, watches · 10 methods',
},
{
name: 'Tasks',
commands: `tasklists, tasks · ${methodCountFor('tasks')} methods`,
},
{
name: 'Docs',
commands: 'documents · 3 methods',
},
{
name: 'Sheets',
commands: 'spreadsheets, values, sheets, developerMetadata · 17 methods',
},
{
name: 'Slides',
commands: 'presentations, pages · 5 methods',
},
{
name: 'Apps Script',
commands: `projects, deployments, versions, processes, scripts · ${methodCountFor('script')} methods`,
},
{
name: 'Analytics Admin',
commands: `accounts, properties, dataStreams, customDimensions, customMetrics, conversionEvents, keyEvents, firebaseLinks, googleAdsLinks · ${methodCountFor('analyticsadmin')} methods`,
},
{
name: 'Analytics Data',
commands: `properties, audienceExports · ${methodCountFor('analyticsdata')} methods`,
},
{
name: 'Search Console',
commands: `searchanalytics, sitemaps, sites, urlInspection, mobileFriendlyTest · ${methodCountFor('searchconsole')} methods`,
},
];
const architecture = [
{
title: 'HTTP/3 when available',
copy: 'The transport probes authenticated QUIC at startup and falls back silently to HTTP/2 when it cannot negotiate.',
glyph: glyphChat,
},
{
title: 'nightly Rust',
copy: 'The CLI enables HTTP/3 through reqwest and QUIC support, which is gated behind Rust nightly and the reqwest_unstable cfg.',
glyph: glyphCalendar,
},
{
title: 'keyring tokens',
copy: 'PKCE is always enabled; tokens are kept in the operating system keyring, with a local file fallback for headless systems.',
glyph: glyphContacts,
},
{
title: 'streaming output',
copy: 'Results go to stdout and logs go to stderr, so jq, scripts, and agents get predictable data by default.',
glyph: glyphMail,
},
];
const sessionExample = `$ grr auth login
{"authenticated":true,"account":null,"token_backend":"os-keyring","token_preview":"ya29.a0AfH6SMBxLqKm..."}
$ grr gmail users messages list --user-id me --q "in:inbox" --max-results 1
{"messages":[{"id":"19f8ab2c","threadId":"19f8ab2c"}],"resultSizeEstimate":1}
$ grr gmail users messages get --user-id me --id 19f8ab2c --param-format metadata
{"id":"19f8ab2c","threadId":"19f8ab2c","labelIds":["UNREAD","INBOX"],"payload":{"headers":[{"name":"Subject","value":"Build notes"}]},"sizeEstimate":4821}`;
const pipeExample = `$ grr gmail users messages list --user-id me --q "from:github.com" --max-results 10 | jq -r '.messages[0].id'
19f8ab2c
$ grr transport
negotiated_protocol: HTTP_3
http3_requested: true
http3_effective: true
fell_back: false
cpus: 10
io_uring: false
http3_always_compiled: true
runtime: tokio multi-thread (auto-sized to cores)`;
const askPlan = `$ grr ask "show my unread gmail"
{"body":null,"confidence":0.88,"dryRun":true,"httpMethod":"GET",
"leastPrivilegeScope":"https://www.googleapis.com/auth/gmail.readonly",
"lowConfidence":false,"method":"gmail.users.messages.list",
"params":{"userId":"me"},"service":"gmail","serviceConfidence":1.0,
"url":"https://gmail.googleapis.com/gmail/v1/users/me/messages"}
# nothing was sent — rerun with --run to execute this request`;
const faqs = [
{
question: 'What is grr?',
answer: `grr is a Rust CLI for Google services from the terminal. One generated command tree covers ${discoveryMethodTotal} methods across the ${discoveryServiceCount} consumer Google Workspace APIs — Gmail, Calendar, Drive, Contacts, Chat, Forms, Tasks, Docs, Sheets, Slides, Apps Script, Analytics Admin, Analytics Data and Search Console — in one namespaced binary with machine-readable output and a single OAuth login.`,
},
{
question: 'How does OAuth work?',
answer: 'Release builds need no setup at all: a Google OAuth client is compiled into the binary, so `grr auth login` just works. Building from source? Run `grr auth setup`, which creates ~/.grr/config.toml for you, or bring your own client. PKCE is always enabled and tokens are kept in the operating system keyring, with a local file fallback for headless systems. `grr auth login --device` prints a URL and code for machines without a browser.',
},
{
question: 'Do I need to log in again after an update?',
answer: 'Once, for this release: the consent set is now derived from the embedded index — every scope any of the 401 methods names, minus the Chat app-authentication scopes Google grants only to a service account — so a token issued for the older, shorter set needs one fresh `grr auth login`. Logins after that keep working across releases until a discovery refresh adds a scope to the index, which needs another re-login.',
},
{
question: 'Is grr affiliated with Google?',
answer: 'No. grr is an independent project. It is not affiliated with, sponsored by, or endorsed by Google LLC. It uses public Google APIs and your own Google account credentials.',
},
{
question: 'How do I install it?',
answer: 'Use a prebuilt GitHub Release, the documented Homebrew package, or the grr-cli crate from crates.io. winget manifests are generated per release, but no public submission has been published yet, so use the Windows release archive today. A crates.io install compiles from source and needs nightly Rust plus the `reqwest_unstable` cfg — see "Why does it need nightly Rust?". The install page has the exact commands for macOS, Linux, Windows x86_64 and Windows on ARM.',
},
{
question: 'Why does it need nightly Rust?',
answer: 'The CLI enables HTTP/3 through reqwest and QUIC support, which is gated behind Rust nightly and the reqwest_unstable cfg. The repo pins the required toolchain and sets the cfg for in-repo builds; the CLI falls back to HTTP/2 at runtime if QUIC is unavailable.',
},
{
question: 'What is `grr api`?',
answer: 'A generic escape hatch generated from Google\'s own Discovery Service, embedded in the binary so it works offline. `grr api list` browses every method, `grr api describe` shows its parameters and scopes, and `grr api call` invokes one by id. Every service in the index — including Tasks, Docs, Sheets and Slides — already has a full typed command tree, so `grr api` is for composing ids dynamically or reaching the long tail of methods without a dedicated namespace. Use --dry-run to see the exact request first.',
},
{
question: 'Can an AI agent use it?',
answer: 'Yes, and the docs are written for that. Output is JSON on stdout with logs on stderr, so `grr ... | jq` and agent tool calls both get predictable data. An agent can discover capabilities with `grr api list` instead of assuming them, inspect any call with --dry-run, and read the exact scope a method declares before calling it — consent itself is granted once, for everything the embedded index names. See the docs page for agents.',
},
];
const storySteps = [
{
index: '01',
title: 'Waves when you arrive.',
copy: `One OAuth login unlocks all ${discoveryServiceCount} Google Workspace APIs in a single namespaced binary.`,
},
{
index: '02',
title: 'Sits with you while you work.',
copy: 'Commands stay close to the work they support, and the output stays parseable.',
},
{
index: '03',
title: 'Sleeps while your scripts run.',
copy: 'No analytics SDK, no telemetry. Tokens rest in the operating system keyring.',
},
{
index: '04',
title: 'Celebrates when you ship.',
copy: 'Output that plays nicely with jq, by default.',
},
];
---
<BaseLayout
title="grr"
description={`grr is a zero-config Rust CLI for Google Workspace: a generated command tree over ${discoveryServiceCount} APIs with ${discoveryMethodTotal} methods, an MCP server, safety profiles, and grr ask turning plain English into a typed plan — clean JSON on stdout.`}
>
<section class="hero section" aria-labelledby="hero-title">
<div class="container hero-grid grid grid-cols-1 items-center gap-[clamp(28px,4vw,64px)] lg:grid-cols-[1.02fr_0.98fr]">
<div class="hero-copy">
<span class="eyebrow hero-fade">Rust Rewrite · all {discoveryServiceCount} APIs · one login</span>
<h1 id="hero-title" class="mt-2.5 mb-4 text-[clamp(30px,2.2rem+2.2vw,54px)] leading-[1.06]">
<span class="hero-line"><span class="hero-line-inner">Google tools,</span></span>
<span class="hero-line"><span class="hero-line-inner hero-line-accent">at terminal speed.</span></span>
</h1>
<p class="hero-lede hero-fade m-0 max-w-[44ch] text-[15px] text-base-content/70">Zero-config, maximum-performance Google tools from the terminal. One binary, all {discoveryServiceCount} Google Workspace APIs, and output that plays nicely with jq.</p>
<div class="hero-actions hero-fade mt-[22px] flex flex-wrap gap-3">
<a class="btn btn-primary" href={releaseUrl} target="_blank" rel="noreferrer">GitHub releases <span aria-hidden="true">↗</span></a>
<a class="btn btn-outline" href={`${base}install/`}>Install grr <span aria-hidden="true">→</span></a>
</div>
<p class="hero-note hero-fade m-0 mt-3.5 text-[13px] text-(--muted)">Release binaries ship with the OAuth client built in — <code>grr auth login</code> and you are in.</p>
</div>
<div class="hero-visual">
<div class="hero-stage">
<div class="hero-terminal-art" set:html={heroTerminal} />
<div class="hero-contact-shadow" aria-hidden="true"></div>
<span class="hero-spark hero-spark-a" aria-hidden="true"></span>
<span class="hero-spark hero-spark-b" aria-hidden="true"></span>
<span class="hero-spark hero-spark-c" aria-hidden="true"></span>
<div class="hero-mascot">
<Mascot label="Pixel-art orange crab mascot waving from the grr terminal" pose="wave" uid="hero" />
</div>
</div>
</div>
</div>
</section>
<InstallMarquee />
<Stats />
<section class="demo-section section" aria-labelledby="demo-title">
<div class="container demo-grid">
<div class="demo-copy reveal">
<span class="eyebrow">watch it work</span>
<h2 id="demo-title">A real terminal. A real agent. {demoDurationSeconds} seconds.</h2>
<p>
Every frame below is grr's actual output: the generated command tree, a live
<code>grr transport</code> probe over HTTP/3, a <code>grr ask</code> plan turning plain
English into a typed method and parameters — and opencode, an agentic harness,
driving grr the way an AI agent would.
</p>
<p class="demo-note">
Recorded once with <code>scripts/generate-demo.ts</code>; no hand-editing, no mocks —
only the pacing is synthetic.
</p>
</div>
<div class="demo-visual">
<DemoPlayer />
</div>
</div>
</section>
<section class="ask-section section" aria-labelledby="ask-title">
<div class="container demo-grid">
<div class="ask-visual reveal">
<CodeBlock code={askPlan} label="grr ask · the plan" copyable={true} />
</div>
<div class="demo-copy reveal">
<span class="eyebrow">the only one you can just ask</span>
<h2 id="ask-title">Plain English in. A typed plan out.</h2>
<p>
<code>grr ask "show my unread gmail"</code> is all it takes. A System One model —
TypeSafe's Jev by default, any provider speaking the same contract works by config —
classifies the request hierarchically: the service ({discoveryServiceCount} options), then the method,
then one typed question per required parameter. Every candidate comes from
the discovery index, so the model can only <em>select</em>. It can never invent a
method, a flag, or a value.
</p>
<p>
What you get back is a plan, not a guess: the resolved method, the parameters, the
URL, the scopes, the confidence — as JSON. Nothing is sent until you say
<code>--run</code>. No other Google Workspace CLI does this; for an AI agent it is
the fastest path from intent to a verifiable request.
</p>
<p class="demo-note">
The safety gate fires the moment the method resolves — <code>--readonly</code> and
friends cover <code>grr ask</code> like everything else.
</p>
</div>
</div>
</section>
<section class="services-section section" id="services" aria-labelledby="services-title">
<div class="container">
<SectionIntro
id="services-title"
eyebrow={`all ${discoveryServiceCount} APIs · one login`}
title="The useful parts of your Google account, namespaced."
copy="Keep commands close to the work they support. Every data command accepts JSON, JSONL, table, or pretty output; JSON is the default."
/>
<div class="feature-rows grid gap-20 lg:gap-24">
{services.map((service, index) => <FeatureRow service={service} reverse={index % 2 === 1} />)}
</div>
<CommandInventory surfaces={commandSurfaces} />
<ApiSurface />
</div>
</section>
<section class="story-section section" aria-labelledby="story-title">
<div class="container">
<div class="section-heading story-heading mb-3">
<span class="eyebrow">scrollytelling</span>
<h2 id="story-title">One crab, four moods.</h2>
</div>
<div class="story-grid grid grid-cols-1 items-start gap-[clamp(32px,5vw,88px)] lg:grid-cols-[minmax(0,1fr)_minmax(260px,380px)]">
<div class="story-steps grid gap-[clamp(160px,28vh,300px)] py-[16vh] max-md:gap-[clamp(96px,18vh,180px)] max-md:py-[8vh]">
{storySteps.map((step) => (
<div class="story-step max-w-[36ch]">
<span class="story-step-index block text-[13px] text-(--accent-text)" aria-hidden="true">// {step.index}</span>
<h3 class="mt-1.5 mb-2 text-[clamp(19px,2.2vw,24px)] leading-[1.2]">{step.title}</h3>
<p class="m-0 text-sm text-base-content/70">{step.copy}</p>
</div>
))}
</div>
<div class="story-stage">
<Mascot variant="story" label="The grr crab mascot waves, waits, sleeps, and celebrates as you scroll" uid="story" />
<div class="story-meter" aria-hidden="true">
<span class="story-tick story-tick-wave"></span>
<span class="story-tick story-tick-idle"></span>
<span class="story-tick story-tick-sleep"></span>
<span class="story-tick story-tick-celebrate"></span>
</div>
<p class="story-captions m-0" aria-hidden="true">
<span class="story-cap story-cap-wave">hi</span>
<span class="story-cap story-cap-idle">your terminal, your data</span>
<span class="story-cap story-cap-sleep">zero config, zero noise</span>
<span class="story-cap story-cap-celebrate">ship it</span>
</p>
</div>
</div>
</div>
</section>
<section class="architecture-section section" aria-labelledby="features-title">
<div class="container">
<div class="architecture-heading reveal max-w-[62ch]">
<span class="eyebrow">built for the shell</span>
<h2 id="features-title" class="mt-1.5 mb-2 text-[clamp(24px,3vw,36px)] leading-[1.12]">Less glue.<br />More signal.</h2>
<p class="m-0 text-sm text-(--muted)">grr is deliberately small at the interface: predictable commands, fast transport, and no hidden service setup.</p>
</div>
<ArchitectureCards cards={architecture} />
<div class="session-panel mt-12 grid grid-cols-1 items-center gap-10 lg:grid-cols-2">
<div class="session-copy reveal">
<span class="eyebrow">one clean session</span>
<h3 class="mt-1.5 mb-2 text-[clamp(19px,2.2vw,24px)] leading-[1.2]">From inbox to insight, without the tab.</h3>
<p class="m-0 text-sm text-base-content/70">Search a message, fetch exactly the format you need, and keep the output parseable. The command tree is available as JSON for agents with <code>grr schema</code>.</p>
<a class="btn btn-outline mt-4" href="https://github.com/debanjanbasu/grr-cli#usage-highlights" target="_blank" rel="noreferrer">Browse command examples <span aria-hidden="true">↗</span></a>
</div>
<div class="session-terminal reveal">
<CodeBlock code={sessionExample} label="terminal · grr" />
</div>
</div>
</div>
</section>
<section class="machine-section section" aria-labelledby="session-title">
<div class="container machine-grid grid grid-cols-1 items-center gap-10 lg:grid-cols-2">
<div class="machine-copy reveal">
<span class="eyebrow">machine-friendly by default</span>
<h2 id="session-title" class="mt-1.5 mb-2 text-[clamp(22px,2.6vw,32px)] leading-[1.15]">Your shell is the API client.</h2>
<p class="m-0 text-sm text-base-content/70">Use a human-readable table when you need it, or keep the default JSON and let the next tool take it from there.</p>
</div>
<div class="machine-terminal reveal">
<CodeBlock code={pipeExample} label="pipe-friendly output" />
</div>
</div>
</section>
<section class="link-band section" aria-labelledby="links-title">
<div class="container">
<div class="section-heading reveal">
<span class="eyebrow">go deeper</span>
<h2 id="links-title">The source is the docs.</h2>
</div>
<div class="link-grid mt-7 grid grid-cols-1 gap-5 md:grid-cols-3">
<a class="link-card reveal" href="https://crates.io/crates/grr-cli" target="_blank" rel="noreferrer">
<strong class="mb-1.5 block text-[15px] text-(--accent-text)">crates.io ↗</strong>
<span class="text-xs text-(--muted)">Package metadata, versions, and the published crate.</span>
</a>
<a class="link-card reveal" href="https://docs.rs/grr-cli" target="_blank" rel="noreferrer">
<strong class="mb-1.5 block text-[15px] text-(--accent-text)">docs.rs ↗</strong>
<span class="text-xs text-(--muted)">Generated API documentation for the published crates.</span>
</a>
<a class="link-card reveal" href="https://github.com/debanjanbasu/grr-cli" target="_blank" rel="noreferrer">
<strong class="mb-1.5 block text-[15px] text-(--accent-text)">GitHub ↗</strong>
<span class="text-xs text-(--muted)">README, releases, source, and the issue tracker.</span>
</a>
</div>
</div>
</section>
<section class="faq-section section" aria-labelledby="faq-title">
<div class="container faq-grid grid grid-cols-1 gap-10 lg:grid-cols-2">
<div class="faq-heading reveal">
<span class="eyebrow">questions, answered</span>
<h2 id="faq-title" class="mt-1.5 mb-2 text-[clamp(22px,2.6vw,32px)] leading-[1.15]">No mystery meat.</h2>
<p class="m-0 text-sm text-(--muted)">The short version of the things people usually want to know before putting a Google account behind a terminal tool.</p>
</div>
<div class="faq-list grid">
{faqs.map((faq) => (
<details class="faq-item collapse collapse-arrow reveal rounded-none border-t border-base-300 last:border-b">
<summary class="collapse-title min-h-12 px-1 text-sm"><InlineCode text={faq.question} /></summary>
<div class="faq-answer collapse-content max-w-[64ch] px-1 text-[13px] leading-[1.7] text-(--muted)"><InlineCode text={faq.answer} /></div>
</details>
))}
</div>
</div>
</section>
<section class="cta-section section" aria-labelledby="cta-title">
<div class="container cta-grid grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
<div class="cta-copy reveal">
<h2 id="cta-title" class="m-0 mb-3 text-[clamp(24px,3vw,36px)] leading-[1.12]">Your Google workflows deserve a better terminal.</h2>
<p class="m-0 text-sm text-base-content/70">Grab a binary, point it at your own OAuth client, and let grr get out of the way.</p>
<div class="hero-actions mt-5 flex flex-wrap gap-3">
<a class="btn btn-primary" href={`${base}install/`}>Choose your install path <span aria-hidden="true">→</span></a>
<a class="btn btn-outline" href={releaseUrl} target="_blank" rel="noreferrer">GitHub releases <span aria-hidden="true">↗</span></a>
</div>
<p class="legal-note m-0 mt-4 text-xs text-(--quiet)">grr is an independent project and is not affiliated with, sponsored by, or endorsed by Google LLC.</p>
</div>
<div class="cta-visual reveal">
<div class="install-slab" role="img" aria-label={'A floating card showing the command: RUSTFLAGS="--cfg reqwest_unstable" cargo install grr-cli'}>
<div class="slab-base" aria-hidden="true"></div>
<code class="slab-face" style="white-space: normal;">$ RUSTFLAGS="--cfg reqwest_unstable" cargo install grr-cli</code>
</div>
<div class="cta-mascot">
<Mascot label="The grr crab mascot celebrating" pose="celebrate" uid="cta" />
</div>
<div class="slab-contact-shadow" aria-hidden="true"></div>
</div>
</div>
</section>
</BaseLayout>