---
import BaseLayout from '../layouts/BaseLayout.astro';
import InlineCode from '../components/InlineCode.astro';
import PageHero from '../components/PageHero.astro';
import benchmarksJson from '../data/benchmarks.json';
import { discoveryMethodTotal, discoveryServiceCount } from '../data/discovery-coverage';
// The snapshot is regenerated by scripts/benchmark.mjs and may legitimately
// be missing any metric (a command absent from the binary, a competitor that
// could not be obtained). Every read below guards for that: the page must
// render the honest subset, never invent the rest.
interface Timing {
available: boolean;
medianMs?: number;
p95Ms?: number;
minMs?: number;
reason?: string;
}
interface Binary {
bytes?: number;
source?: string;
form?: string;
available?: boolean;
reason?: string;
// The release artifact users install, when the snapshot could measure it.
// Competitors are only ever measured as shipped, so they carry no
// `shipped` block of their own.
shipped?: Shipped;
}
interface Shipped {
bytes?: number;
source?: string;
form?: string;
packed?: boolean;
releaseTag?: string;
asset?: string;
}
interface Memory {
available?: boolean;
maxRssKb?: number;
runs?: number;
command?: string;
reason?: string;
}
interface CompetitorRow {
tool: string;
project?: string;
url?: string;
available: boolean;
obtainedVia?: string;
version?: string;
reason?: string;
startup?: Timing;
schemaDump?: Timing;
apiList?: Timing;
memory?: Memory;
binary?: Binary;
}
interface Benchmarks {
generatedAt?: string;
environment?: {
platform?: string;
arch?: string;
nodeVersion?: string;
binaryKind?: string;
binaryPath?: string;
grrVersion?: string;
};
versions?: Record<string, { local?: string | null; latestTag?: string | null; latestTagReason?: string }>;
protocol?: { runs?: number; warmupRunsDiscarded?: number; stability?: string; memoryTimer?: string; memoryWorkload?: string };
grr?: {
startup?: Timing;
schemaDump?: Timing;
apiList?: Timing;
memory?: Memory;
binary?: Binary;
};
competitors?: CompetitorRow[];
}
const benchmarks = benchmarksJson as Benchmarks;
const { environment, protocol } = benchmarks;
const versions = benchmarks.versions ?? {};
const grr = benchmarks.grr ?? {};
const competitors = Array.isArray(benchmarks.competitors) ? benchmarks.competitors : [];
const availableCompetitors = competitors.filter((row) => row.available && row.startup?.available);
const unavailableCompetitors = competitors.filter((row) => !availableCompetitors.includes(row));
// Latest release tags recorded by the benchmark: shown next to the measured
// versions so the page states both what ran and where each project stood.
const latestReleases = ['grr', 'gog', 'gws']
.map((tool) => (versions[tool]?.latestTag ? `${tool} ${versions[tool].latestTag}` : null))
.filter((entry): entry is string => Boolean(entry))
.join(' · ');
const generatedAt = benchmarks.generatedAt ?? '';
const generatedDate = new Date(generatedAt);
const generatedLabel = Number.isNaN(generatedDate.getTime())
? 'unknown'
: generatedDate.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' });
const ms = (value: number | undefined) => (Number.isFinite(value) ? `${value} ms` : '—');
const megabytes = (bytes: number | undefined) =>
typeof bytes === 'number' && Number.isFinite(bytes) ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : '—';
// One binary-size rule for both the numeric table and the feature matrix.
//
// A measurement is only a size when it measured the tool itself. gws ships
// through npm, so the executable PATH resolves to is a ~852-byte launcher
// shim (run.js) that spawns the real artifact elsewhere — the benchmark even
// records its source as "installed on PATH" rather than a build output path
// like grr's "target/release/grr". Quoting that shim as gws's binary size put
// a 852-byte file against a 10 MB executable and rendered as "0.0 MB".
//
// No standalone Workspace CLI binary is under a mebibyte, so a measurement
// below that is a launcher shim (or otherwise not the binary) and is rendered
// as an em dash with the reason, never as a number.
const MIN_BINARY_BYTES = 1024 * 1024;
const isLauncherShim = (binary: Binary | undefined) => {
const bytes = binary?.bytes;
return typeof bytes === 'number' && Number.isFinite(bytes) && bytes > 0 && bytes < MIN_BINARY_BYTES;
};
const trustworthyBytes = (binary: Binary | undefined): number | null => {
const bytes = binary?.bytes;
if (typeof bytes !== 'number' || !Number.isFinite(bytes) || bytes <= 0) return null;
return bytes >= MIN_BINARY_BYTES ? bytes : null;
};
const sizeDetail = (binary: Binary | undefined) =>
isLauncherShim(binary)
? `measured artifact is a ${binary?.bytes}-byte launcher shim, not a binary (${binary?.source ?? 'unknown source'})`
: binary?.reason ?? 'not measured';
// The size a user actually installs. grr's release archive holds the
// UPX-packed binary when the packer succeeded (and an unpacked one when it
// did not), while the benchmark's own timings run a locally built (unpacked)
// binary, so the snapshot carries both; competitors are only ever measured as
// shipped. This is the one rule behind both the numeric table and the feature
// matrix, so the two can never disagree about which number leads.
const shippedBytes = (binary: Binary | undefined): number | null => {
const bytes = binary?.shipped?.bytes;
return typeof bytes === 'number' && Number.isFinite(bytes) && bytes > 0 ? bytes : null;
};
const comparableSize = (binary: Binary | undefined): number | null => shippedBytes(binary) ?? trustworthyBytes(binary);
// The tooltip that keeps the other figure visible: "as shipped" as the
// headline, the unpacked build (and the release tag it came from) in the
// detail. Returns undefined when no shipped artifact was measured, so the
// cell falls back to the pre-existing shim/reason discipline.
const asShippedDetail = (binary: Binary | undefined): string | undefined => {
const shipped = shippedBytes(binary);
if (shipped === null) return undefined;
const label = binary?.shipped?.source ?? 'as shipped';
const tag = binary?.shipped?.releaseTag ? ` ${binary.shipped.releaseTag}` : '';
const unpacked = trustworthyBytes(binary);
return binary?.form === 'binary' && unpacked !== null && unpacked !== shipped
? `${label}${tag}; ${megabytes(unpacked)} unpacked as built`
: `${label}${tag}`;
};
// Table cell: the number, or an em dash whose tooltip carries the reason. The
// cell is never blank.
const measuredSize = (binary: Binary | undefined) => {
const bytes = comparableSize(binary);
if (bytes === null) return { bytes: null as number | null, text: '—', title: sizeDetail(binary) };
return { bytes, text: megabytes(bytes), title: asShippedDetail(binary) };
};
// Peak memory: one high-water mark per tool, never a per-cell median.
const memoryMb = (metric: Memory | undefined) =>
metric?.available && typeof metric.maxRssKb === 'number' && Number.isFinite(metric.maxRssKb) && metric.maxRssKb > 0
? `${(metric.maxRssKb / 1024).toFixed(1)} MB`
: '—';
const memoryDetail = (metric: Memory | undefined) => {
if (!metric?.available) return `not in this snapshot — ${metric?.reason ?? 'not measured'}`;
const runs = typeof metric.runs === 'number' ? ` — max of ${metric.runs} runs` : '';
return `peak RSS${runs} for ${metric.command ?? 'the schema-dump command'}`;
};
const timingDetail = (metric: Timing | undefined) => {
if (!metric?.available) return `not in this snapshot — ${metric?.reason ?? 'not measured'}`;
return `p95 ${ms(metric.p95Ms)} · min ${ms(metric.minMs)}`;
};
// grr-facing cards: whatever the snapshot actually contains.
const grrSizeIsShipped = shippedBytes(grr.binary) !== null;
const cards = [
{ label: 'startup · grr --version', value: grr.startup?.available ? ms(grr.startup?.medianMs) : '—', detail: timingDetail(grr.startup) },
{ label: 'schema dump · grr schema', value: grr.schemaDump?.available ? ms(grr.schemaDump?.medianMs) : '—', detail: timingDetail(grr.schemaDump) },
{ label: 'api index · grr api list', value: grr.apiList?.available ? ms(grr.apiList?.medianMs) : '—', detail: timingDetail(grr.apiList) },
{
// The label names the basis, so an old snapshot that has no shipped
// artifact can never present the unpacked build as the shipped size.
label: grrSizeIsShipped ? 'binary size · as shipped' : 'binary size · unpacked build',
value: megabytes(comparableSize(grr.binary) ?? undefined),
detail: asShippedDetail(grr.binary)
?? (grr.binary?.form === 'binary' ? 'unpacked executable' : grr.binary?.form === 'release-archive' ? 'release archive' : grr.binary?.source ?? ''),
},
];
// Columns of the numeric comparison: grr plus every competitor that actually
// produced startup numbers on this snapshot.
const columns = [
{ name: 'grr', version: environment?.grrVersion ?? '', startup: grr.startup, schemaDump: grr.schemaDump, apiList: grr.apiList, memory: grr.memory, binary: grr.binary },
...availableCompetitors.map((row) => ({ name: row.tool, version: row.version ?? '', startup: row.startup, schemaDump: row.schemaDump, apiList: row.apiList, memory: row.memory, binary: row.binary })),
];
// The binary-size row of the feature matrix reads the same measured snapshot
// as the numeric table above, so the two can never disagree.
const gogRow = competitors.find((row) => row.tool === 'gog');
const gwsRow = competitors.find((row) => row.tool === 'gws');
const asShippedSize = (binary: Binary | undefined) => {
const bytes = comparableSize(binary);
if (bytes !== null) return megabytes(bytes);
return isLauncherShim(binary) ? `— launcher shim, not a binary (${binary?.bytes} B)` : '—';
};
const comparableBytes = [grr.binary, gogRow?.binary, gwsRow?.binary]
.map((binary) => comparableSize(binary))
.filter((bytes): bytes is number => bytes !== null);
const grrComparable = comparableSize(grr.binary);
// "Smallest of the three" compares like with like: each tool's shipped
// artifact where the snapshot has one, else its own measured build (a
// competitor's bytes are always its official release build, i.e. as shipped).
// It is only claimed when all three produced a trustworthy measurement; a
// shim or a missing size can never win the title.
const grrSmallest = grrComparable !== null && comparableBytes.length === 3 && Math.min(...comparableBytes) >= grrComparable;
// Disclosed in the row whenever grr's headline size is not a shipped artifact
// or the "smallest" claim rests on the unpacked build.
const grrSizeBasis = grrSizeIsShipped
? (grrSmallest ? ' — smallest of the three, as shipped' : '')
: grrComparable !== null
? ' — unpacked build (no release artifact in this snapshot)'
: '';
const grrSizeTitle = grrSizeIsShipped
? asShippedDetail(grr.binary)
: grrComparable !== null
? `unpacked build — no release artifact measured in this snapshot (${megabytes(grrComparable)})`
: sizeDetail(grr.binary);
// ---------------------------------------------------------------------------
// Feature matrix. Every cell cites the project's own documentation or a
// running release binary (sources listed under the table). Cells we could
// not verify say "Not documented" — that is a claim about their docs, not a
// measured absence in the tool.
// ---------------------------------------------------------------------------
interface Cell {
text: string;
kind?: 'yes' | 'no' | 'gap' | 'na';
// Optional tooltip carrying the detail a headline cannot (e.g. the unpacked
// figure behind an as-shipped size). Rendered on the <td>.
title?: string;
}
const cellClass = (kind: Cell['kind']) => {
switch (kind) {
case 'yes':
return 'text-base-content';
case 'gap':
return 'font-bold text-(--accent-text)';
case 'no':
case 'na':
return 'text-(--quiet)';
default:
return 'text-base-content/70';
}
};
const matrix: { label: string; cells: [Cell, Cell, Cell, Cell] }[] = [
{
label: 'Coverage',
cells: [
{
text: `${discoveryMethodTotal} methods across the ${discoveryServiceCount} consumer Workspace APIs — every Workspace API a consumer account can actually authorise (domain-locked services like Classroom and Meet are deliberately out of scope) — generated from a Discovery index committed to the repo, works offline`,
kind: 'yes',
},
{ text: '27 services, incl. Classroom, Meet, Analytics, Ads, YouTube', kind: 'yes' },
{ text: 'Every Workspace API — command surface built from Google’s live Discovery Service at runtime', kind: 'yes' },
{ text: 'n/a — fixed per-product toolsets', kind: 'na' },
],
},
{
label: 'Natural-language entry',
cells: [
{
text: 'Yes — grr ask "<request>": a System One model (Jev by default, provider-agnostic config) picks service → method and fills each required parameter from code-supplied candidates via one typed question; the plan prints by default, --run executes it. Neither competitor documents an equivalent',
kind: 'yes',
},
{ text: 'Not documented', kind: 'na' },
{ text: 'Not documented', kind: 'na' },
{ text: 'n/a — the MCP client is the interface', kind: 'na' },
],
},
{
label: 'MCP server',
cells: [
{
text: `Yes — grr mcp: all ${discoveryMethodTotal} methods as typed MCP tools over stdio (inputSchema from Discovery); --readonly for a read-only server; tools/call runs the same call path as grr api call`,
kind: 'yes',
},
{ text: 'Yes — gog mcp, typed stdio, read-only by default', kind: 'yes' },
{ text: 'No — v0.22.5 has no mcp command; no MCP section in its README', kind: 'no' },
{ text: 'Is one — hosted remote servers (Developer Preview)', kind: 'yes' },
],
},
{
label: 'Agent skills',
cells: [
{
text: 'Yes — packaged skills/grr/SKILL.md (discover-first discipline, the naming rule, the output contract); npx skills add https://github.com/debanjanbasu/grr-cli',
kind: 'yes',
},
{ text: 'Yes — schema, reference pages and skills generated from the same command tree', kind: 'yes' },
{ text: 'Yes — 100+ SKILL.md files, one per API, plus 50 curated recipes', kind: 'yes' },
{ text: 'n/a — agents connect directly via MCP', kind: 'na' },
],
},
{
label: 'Multi-account',
cells: [
{ text: 'Yes — named accounts (--account work) keep a separate token each in the OS keyring', kind: 'yes' },
{ text: 'Yes — accounts, aliases, keyring-stored tokens', kind: 'yes' },
{ text: 'Not documented', kind: 'na' },
{ text: 'n/a', kind: 'na' },
],
},
{
label: 'Safety profiles',
cells: [
{ text: 'Global --readonly, --deny-service and --deny-verb flags, honored by every entry point: the command tree, grr api call, grr ask and grr mcp', kind: 'yes' },
{ text: 'Global --readonly and --no-input, exact command allowlists, Gmail no-send policy, dry-run plans; build-time Safety Profiles documented', kind: 'yes' },
{ text: 'Not documented', kind: 'na' },
{ text: 'n/a — per-server OAuth scopes', kind: 'na' },
],
},
{
label: 'HTTP/3 (QUIC)',
cells: [
{ text: 'Always on, HTTP/2 fallback — the only tool in this table that documents HTTP/3', kind: 'yes' },
{ text: 'Not documented', kind: 'na' },
{ text: 'Not documented', kind: 'na' },
{ text: 'n/a', kind: 'na' },
],
},
{
label: 'Zero-config OAuth',
cells: [
{ text: 'Yes — release binaries ship the client compiled in (source builds run grr auth setup) — the only one in this table', kind: 'yes' },
{ text: 'No — create your own Desktop OAuth client in Google Cloud', kind: 'no' },
{ text: 'No — GCP project + OAuth client required (auth setup)', kind: 'no' },
{ text: 'No — own GCP project, OAuth consent screen and Web client per server', kind: 'no' },
],
},
{
label: 'Runtime dependencies',
cells: [
{ text: 'None — single static binary', kind: 'yes' },
{ text: 'None — single binary', kind: 'yes' },
{ text: 'Node 18+ for npm install; pre-built binaries also published' },
{ text: 'No CLI to install — any MCP client', kind: 'na' },
],
},
{
label: 'Binary size (as shipped)',
cells: [
{ text: `${asShippedSize(grr.binary)}${grrSizeBasis}`, kind: 'yes', title: grrSizeTitle },
{ text: asShippedSize(gogRow?.binary), title: asShippedDetail(gogRow?.binary) },
{ text: asShippedSize(gwsRow?.binary), title: asShippedDetail(gwsRow?.binary) },
{ text: 'n/a — hosted service', kind: 'na' },
],
},
{
label: 'Language',
cells: [
{ text: 'Rust' },
{ text: 'Go' },
{ text: 'Rust' },
{ text: 'Hosted service', kind: 'na' },
],
},
{
label: 'License',
cells: [
{ text: 'MIT' },
{ text: 'MIT' },
{ text: 'Apache-2.0' },
{ text: 'Developer Preview terms', kind: 'na' },
],
},
];
const columnHeads = ['grr', 'gog', 'gws', 'Google Workspace MCP servers'];
---
<BaseLayout
title="Compare"
description="Startup, schema-dump and api-index timings plus shipped binary sizes and peak memory for grr, gog and gws, measured daily in CI without credentials, and a feature matrix verified against each project's own docs."
>
<PageHero eyebrow="honest comparison" title="Compare">
grr measured against the other Google Workspace CLIs. Every number below was actually run — no vendor benchmarks, no estimates. Where grr is behind, the table says so; where a claim could not be verified, it says that instead.
<Fragment slot="aside">snapshot <time datetime={generatedAt}>{generatedLabel}</time></Fragment>
</PageHero>
<section class="section" aria-labelledby="read-title">
<div class="container">
<div class="section-heading reveal">
<div>
<span class="eyebrow">method</span>
<h2 id="read-title">How to read this</h2>
</div>
</div>
<div class="compare-read max-w-[72ch]">
<p>
Measured metrics are credential-free by design: process startup (<code>--version</code>), the
<code>grr schema</code> JSON contract dump, the <code>grr api list</code> method index,
binary size on disk and peak memory. Nothing here needs a Google account or a network
round-trip, which is what makes a daily CI benchmark possible without flakes.
</p>
<p>
Protocol per metric: the first run is a discarded warm-up, then
{protocol?.runs ?? 15}
timed runs using <code>process.hrtime.bigint()</code> around the process; median, p95 and
min are reported. Every tool gets the identical protocol on the same machine. A run whose
medians land within the noise band (max(15 ms, 6%)) of the committed snapshot rewrites
nothing, so small differences are noise, not news. The benchmark re-runs whenever a new
version of grr, gog or gws ships — the workflow watches each project's release tags and
rebuilds on change — so the numbers follow the tools rather than a one-time snapshot.
</p>
<p>
Peak memory is <code>max RSS</code>: the maximum resident set size across the same runs,
read from the platform's <code>/usr/bin/time</code> (GNU <code>-v</code> on Linux, BSD
<code>-l</code> on macOS) while each tool runs its schema-dump command —{' '}
{protocol?.memoryWorkload ?? 'each tool’s closest equivalent workload'}. It is a single
high-water mark per run, so the highest is reported rather than a median. This is the
figure that competes with local models and agents sharing unified memory: every megabyte a
CLI holds is one they cannot. All memory figures come from the same locally built
(unpacked) grr binary as the timings; the packed release artifact is not separately
re-measured for memory, and that basis is stated rather than assumed.
</p>
<p>
Binary size is the number users actually install. grr's release artifact is UPX-packed when
the packer succeeds, so the size shown is the shipped artifact, with the unpacked build in
the tooltip; the timings and peak memory above come from the unpacked build, which is what
the benchmark runs. Competitor sizes are their official release builds, as shipped — so the
"smallest" claim compares like with like. When a snapshot has no release artifact (no tag
yet, or offline), the row falls back to the unpacked build and says so.
</p>
<p>
Feature matrix cells are verified against each project's own documentation and, where
possible, a running release binary — sources are listed under the table. "Not documented"
means exactly that: the claim could not be verified from their docs, so it is neither
confirmed nor denied.
</p>
</div>
<dl class="compare-env mt-5 flex flex-wrap gap-x-10 gap-y-3 border border-base-300 bg-base-100 p-4">
<div>
<dt class="text-[11px] uppercase text-(--quiet)">measured on</dt>
<dd class="m-0 mt-1 text-[13px]">{environment?.platform ?? 'unknown'} / {environment?.arch ?? 'unknown'}</dd>
</div>
<div>
<dt class="text-[11px] uppercase text-(--quiet)">grr binary</dt>
<dd class="m-0 mt-1 text-[13px]">{environment?.binaryKind ?? 'none'}{environment?.grrVersion ? ` · ${environment.grrVersion}` : ''}</dd>
</div>
<div>
<dt class="text-[11px] uppercase text-(--quiet)">latest releases</dt>
<dd class="m-0 mt-1 text-[13px]">{latestReleases || 'unknown'}</dd>
</div>
<div>
<dt class="text-[11px] uppercase text-(--quiet)">node</dt>
<dd class="m-0 mt-1 text-[13px]">{environment?.nodeVersion ?? 'unknown'}</dd>
</div>
<div>
<dt class="text-[11px] uppercase text-(--quiet)">warm-up</dt>
<dd class="m-0 mt-1 text-[13px]">{protocol?.warmupRunsDiscarded ?? 1} run discarded per metric</dd>
</div>
</dl>
</div>
</section>
<section class="section" aria-labelledby="measured-title">
<div class="container">
<div class="section-heading reveal">
<div>
<span class="eyebrow">measured, not guessed</span>
<h2 id="measured-title">The numbers</h2>
</div>
<p>Medians of {protocol?.runs ?? 15} runs on the machine named above. A missing card value means the command is not present in the measured binary — the reason is printed, never padded.</p>
</div>
<div class="stats stats-vertical w-full border border-base-300 bg-base-100 sm:stats-horizontal">
{
cards.map((card) => (
<div class="stat stat-card">
<div class="stat-title stat-label">{card.label}</div>
<div class="stat-value">{card.value}</div>
<div class="stat-desc"><InlineCode text={card.detail} /></div>
</div>
))
}
</div>
{
columns.length > 1 ? (
<div class="compare-table-wrap mt-7 overflow-x-auto border border-base-300 bg-base-100">
<table class="compare-table table table-zebra">
<caption class="stat-label">
startup (<code>--version</code>), schema-dump, api-index, peak RSS and {grrSizeIsShipped ? 'binary size as shipped' : 'binary size'} — identical protocol per tool, where an equivalent exists.{' '}
{grrSizeIsShipped
? "grr's size is its shipped release artifact (UPX-packed when the packer succeeded); its timings and peak RSS come from the locally built unpacked binary, so UPX's one-time exec decompression is not part of them."
: "grr's timings, peak RSS and size all come from the same locally built binary."}
</caption>
<thead>
<tr>
<th scope="col">tool</th>
<th scope="col">startup median</th>
<th scope="col">startup p95</th>
<th scope="col">startup min</th>
<th scope="col">schema dump</th>
<th scope="col">api index</th>
<th scope="col">peak memory</th>
<th scope="col">binary size</th>
</tr>
</thead>
<tbody>
{columns.map((column) => {
const size = measuredSize(column.binary);
return (
<tr>
<th scope="row">
{column.name}
{column.version && <span class="compare-version mt-0.5 block text-[11px] font-normal text-(--quiet)">{column.version}</span>}
</th>
<td>{ms(column.startup?.available ? column.startup?.medianMs : undefined)}</td>
<td>{ms(column.startup?.available ? column.startup?.p95Ms : undefined)}</td>
<td>{ms(column.startup?.available ? column.startup?.minMs : undefined)}</td>
<td title={column.schemaDump?.available ? undefined : `not comparable — ${column.schemaDump?.reason ?? 'not measured'}`}>
{ms(column.schemaDump?.available ? column.schemaDump?.medianMs : undefined)}
</td>
<td title={column.apiList?.available ? undefined : `not comparable — ${column.apiList?.reason ?? 'not measured'}`}>
{ms(column.apiList?.available ? column.apiList?.medianMs : undefined)}
</td>
<td title={column.memory?.available ? memoryDetail(column.memory) : `not comparable — ${column.memory?.reason ?? 'not measured'}`}>
{memoryMb(column.memory)}
</td>
<td title={size.title}>{size.text}</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : (
<p class="compare-absent m-0 mt-6 max-w-[72ch] text-sm text-base-content/70">
No competitor tool could be obtained and run for this snapshot, so the numeric comparison
shows grr alone. Nothing was invented for tools that could not run — that is the point.
The benchmark keeps trying on every daily run and the table appears as soon as one does.
</p>
)
}
{
unavailableCompetitors.length > 0 && (
<ul class="compare-unavailable m-0 mt-3.5 list-disc pl-5 text-xs text-(--quiet)">
{unavailableCompetitors.map((row) => (
<li>
<code>{row.tool}</code> unavailable for this snapshot — <InlineCode text={row.reason ?? ''} />
</li>
))}
</ul>
)
}
<p class="stats-footnote">
<span class="status-dot" aria-hidden="true"></span>
<span>
Snapshot generated <time datetime={generatedAt}>{generatedLabel}</time>
{environment?.binaryKind === 'release' || environment?.binaryKind === 'debug'
? ` from a locally built ${environment.binaryKind} binary`
: ''}
. Competitor binaries are their official GitHub release builds, as shipped
{grrSizeIsShipped
? '; grr’s size is its own shipped release artifact, its timings and memory the unpacked build'
: ''}.
</span>
<a href="https://github.com/debanjanbasu/grr-cli/blob/main/scripts/benchmark.ts" target="_blank" rel="noreferrer">
The benchmark script <span aria-hidden="true">→</span>
</a>
</p>
</div>
</section>
<section class="section" aria-labelledby="matrix-title">
<div class="container">
<div class="section-heading reveal">
<div>
<span class="eyebrow">feature matrix</span>
<h2 id="matrix-title">What each tool actually offers</h2>
</div>
<p>
Verified against each project’s own documentation and running binaries — grr’s cells
were re-verified against the current build. Where grr trails, the row says so; where it
leads, the row says that too.
</p>
</div>
<div class="compare-table-wrap mt-7 overflow-x-auto border border-base-300 bg-base-100">
<table class="compare-table compare-matrix table table-zebra">
<thead>
<tr>
<th scope="col">feature</th>
{columnHeads.map((head) => (
<th scope="col">{head}</th>
))}
</tr>
</thead>
<tbody>
{
matrix.map((row) => (
<tr>
<th scope="row" class="font-normal">{row.label}</th>
{row.cells.map((cell) => (
<td class={cell.kind ? cellClass(cell.kind) : 'text-base-content/70'} title={cell.title}>{cell.text}</td>
))}
</tr>
))
}
</tbody>
</table>
</div>
<div class="compare-read mt-6 max-w-[72ch]">
<p>
Where the others still win: <a href="https://github.com/openclaw/gogcli" target="_blank" rel="noreferrer">gog</a> covers 27
services — including domain-locked enterprise APIs like Classroom and Meet that a consumer
account cannot authorise and that grr deliberately leaves out.
<a href="https://github.com/googleworkspace/cli" target="_blank" rel="noreferrer">gws</a> covers every Workspace API, not
just the consumer {discoveryServiceCount} — its command surface is rebuilt from Google's live Discovery
Service, so when Google adds a method, gws has it without a release — and it ships 100+
agent skills. Google's
<a href="https://developers.google.com/workspace/guides/configure-mcp-servers" target="_blank" rel="noreferrer">
remote MCP servers
</a>
need no local install at all.
</p>
<p>
Where grr goes further: <code>grr ask</code> takes a request in plain words; a System One
model (Jev by default) classifies the service and the method, then fills each required
parameter by asking one typed question with every candidate code-supplied — the model can
only select. Neither competitor documents anything like it. <code>grr mcp</code> exposes
all {discoveryMethodTotal} methods as typed MCP tools and runs them through the same call path as
<code>grr api call</code>, behind the same <code>--readonly</code>,
<code>--deny-service</code> and <code>--deny-verb</code> gates as the rest of the CLI.
Named accounts keep separate tokens in the OS keyring. HTTP/3 is always on with HTTP/2
fallback, release binaries ship the OAuth client compiled in so the first run is
zero-config{grrSmallest ? ', and as shipped it is the smallest of the three' : ''} — the size row above is
computed from the same snapshot, not typed by hand.
</p>
{
columns.some((column) => column.memory?.available) && (
<p>
Peak memory is the figure that competes with the local models and agents sharing
unified memory on a modern machine: every megabyte a CLI holds is one they cannot. On
this snapshot the schema dump peaks at{' '}
{columns.map((column) => `${column.name} ${memoryMb(column.memory)}`).join(' · ')} of
resident memory — each tool's own closest-equivalent workload, measured rather than
assumed, and never a synthetic one.
</p>
)
}
<p>
The original bet is unchanged: the command tree is generated from a Discovery index
committed to the repository — reviewable in a diff, working offline, refreshed by a
daily workflow — rather than rebuilt from live Discovery at runtime.
</p>
<p class="compare-sources text-(--quiet)">
Sources: gog cells from its README (quick start, accounts, MCP guide, safety) plus
<code>gog auth services</code> and <code>gog --help</code> on the v0.43.0 release binary;
gws cells from its README and <code>gws --help</code> / <code>gws auth --help</code> on the
0.22.5 release binary — which answers "Unknown service mcp" to <code>gws mcp</code> — and
its LICENSE file; Google server cells from Google's configure-mcp-servers guide
(Developer Preview). grr cells re-verified against the current binary:
<code>grr mcp --help</code>, a live initialize + tools/list handshake ({discoveryMethodTotal} tools, each
with an inputSchema), <code>grr ask --help</code>, <code>grr auth login --help</code>,
<code>grr auth status --help</code>, and <code>--readonly</code> / <code>--deny-verb</code>
refusal checks. The binary-size and peak-memory rows read the same measured snapshot as
the table above.
Unverifiable claims are labelled "Not documented" rather than guessed.
</p>
</div>
</div>
</section>
</BaseLayout>