An OpenCode plugin that authenticates against Google Antigravity (Cloud Code Assist) via OAuth using a dual-bypass identity (Antigravity headers plus Gemini CLI identification spoof) and rotates across multiple Google accounts with a robin-hood soft-quota strategy.
maene (@wenathlan/maene) is an OpenCode plugin, a CLI, and a library-first TypeScript module set that lets OpenCode talk to Google Antigravity — also known as Google Cloud Code Assist at cloudcode-pa.googleapis.com — using plain Google OAuth tokens, with no user-supplied GCP project id. It impersonates the official Gemini CLI client identification (client id 681255809395-oo8f…b135j, User-Agent GeminiCLI/0.35.3, X-Goog-Api-Client: gl-node/22.19.0, Client-Metadata: ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI) and falls back to the shared public project rising-fact-p41fc when no cloudaicompanionProject can be discovered through loadCodeAssist.
The bypass has two independent faces that share the same JSON identification, headers, and User-Agent. The first face, the Antigravity bypass, wraps an OpenCode request as { project, model, request } and posts it to cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse with antigravity-style headers (User-Agent: antigravity/<v> <os>/<arch>). The second face, the Gemini CLI identification spoof, sends the same envelope with the exact Gemini CLI identification headers and is used for gemini-cli-only models (gemini-3, gemini-3.1, gemini-3.5, gemini-3.6, gemini-3.7, preview and customtools variants) or when cli_first is true and the model is a Gemini, which preserves the Antigravity quota for Claude calls.
On top of the bypass, maene adds a robin-hood multi-account rotation layer. A single v3 account store at ~/.config/opencode/antigravity-accounts.json (chmod 0600, atomic write) holds multiple Google accounts, each with its own refresh token, per-model rate-limit reset times, and soft or hard quota cooldowns. The plugin rotates between accounts round-robin (or sticky) at the 90 percent soft-quota threshold, falls back to the not-rate-limited pool, then to the full active pool, so a request is always served rather than 401'd. The runtime constraint is strict: only node:* builtins plus global fetch. Zero npm runtime dependencies. Peers @opencode-ai/sdk and @opencode-ai/plugin are optional.
| Keyword | Feature | Module(s) |
|---|---|---|
antigravity, gemini, google, claude, gemini-3, gemini-3.1, claude-opus-4-6 |
Frozen 2026 model catalog (gemini 2.5, 3, 3.1, 3.5, 3.6, 3.7, claude 4-5, 4-6, gpt-oss-120b, plus antigravity aliases) | models.ts (v2.1.15: model identification lives ONLY here — never in any other file), plugin.ts |
opencode, opencode-plugin, cloud-code-assist |
OpenCode plugin contract (default export, getPlugin() singleton, auth hooks, fetch interceptor) |
plugin.ts, index.ts |
auth, oauth, gemini-cli |
OAuth 2.0 PKCE against Google with the Gemini CLI client id, local 127.0.0.1 callback server, refresh-token rotation | auth.ts (v2.1.15: THE auth owner — every OAuth client identity lives only here) |
bypass, dual-bypass |
Project-id bypass through loadCodeAssist plus onboardUser cascade plus Gemini CLI identification spoof |
plugin.ts, auth.ts, core.ts, project.ts |
robin-hood, multi-account, dual-quota, soft-quota |
v3 account store, rotation, soft 90 percent threshold, rate-limit reset times, dual-pool cli_first routing |
accounts.ts, auth.ts, plugin.ts, quota.ts |
thinking-cache |
LRU(100) thinking-signature cache (preserves Claude thoughtSignature across turns) |
core.ts, streaming.ts, recovery.ts |
google-search |
googleSearch plus urlContext tool injection, grounding-chunk citation extraction, markdown rendering |
search.ts, request.ts, core.ts |
image-generation |
gemini-3.1-flash-image model, BLOCK_NONE safety, configurable aspect ratio, base64 image persistence |
plugin.ts, core.ts, search.ts, request.ts |
schema-cleaner |
Recursive JSON-schema sanitizer (10 allowed keys, additionalProperties and defaults rewritten into descriptions) | core.ts, request.ts |
version-dynamic |
Two-tier dynamic version resolver (auto-updater plus npm plus GitHub plus storage.googleapis.com, governed fallback 1.19.2, blocked list) | version.ts, config.ts, constants.ts |
tool-hardening |
Tool-name regex /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/, dedupe, max 128 tools, Claude [hardening] description prefix |
core.ts, request.ts, plugin.ts |
9router, omniroute |
Logical to physical model mapping plus PID-offset anti-fingerprint (0 to 1000 ms user_prompt_id randomization) |
models.ts, plugin.ts |
llm-gateway |
OpenAI and Anthropic to Gemini contents transform, SSE parser, dual output format transformers | streaming.ts, request.ts, plugin.ts |
maene |
Plugin id plus alternative name (config alternativeName) |
index.ts, package.json |
- Node
>=18(developed and tested against Node 26.7.0 in CI). - bun
>=1.0(developed against bun 1.2.0; required for the lint, build, and test scripts). - An OpenCode installation (optional — required only for plugin mode).
- One or more Google accounts willing to grant OAuth consent to the Gemini CLI public client id.
npm install @wenathlan/maene
# or
bun add @wenathlan/maene
# or
pnpm add @wenathlan/maeneGlobal CLI install (provides the maene bin):
npm install -g @wenathlan/maenePeer dependencies (optional but required at runtime for plugin mode): @opencode-ai/sdk ^1.18.3, @opencode-ai/plugin >=0.13.0. The published artifact is the root .ts files (per package.json files), with plugin.ts as main, types, module, and exports["."].
The bin entry is maene backed by cli.ts. Subcommands:
| Command | Action |
|---|---|
maene login |
Run OAuth 2.0 PKCE against Google, capture a refresh token, add the account to the v3 store. |
maene logout [email] [--all] |
Revoke refresh tokens and remove account(s) from the store. |
maene accounts list |
Print the v3 store: addedAt, createdAt, expiry, projectId, remaining, limit, rateLimitResetTimes, soft-quota cooldowns. |
maene accounts add --email <email> --refresh-token <token> [--project <id>] |
Add an account non-interactively. |
maene enable <email> / maene disable <email> |
Toggle an account in or out of robin-hood rotation. |
maene manage |
Interactive per-account enable/disable TUI. |
maene quota [email] [--all] [--json] |
Retrieve dual-source quota (Antigravity plus Gemini CLI) per account. |
maene config {list,get,set,reset} |
Inspect or mutate ~/.config/opencode/antigravity.json. |
maene configure [--global] [--path <file>] |
Write the 2026-08-25 model definitions into opencode.json (chmod 0600 atomic write). |
maene models {list,info} |
Browse the frozen 2026 catalog. |
maene status |
Show active account, version, and endpoint health. |
maene doctor |
File, permission, and token-expiry diagnostics. |
maene version [--json] |
Print the resolved maene plus dynamic Antigravity versions. |
maene menu / maene interactive |
Open the interactive menu. |
maene help |
Print the help screen. |
Example session:
maene login # browser opens, OAuth PKCE completes, refresh_token stored
maene accounts list # see the first account in the v3 store
maene login # log in again with a second Google account
maene quota --all # compare remaining quota across accounts
maene configure --global # write 2026 model defs into ~/.config/opencode/opencode.json
maene doctor # sanity-check permissions and token expiryEnvironment overrides:
OPENCODE_CONFIG_DIR— overrides the default~/.config/opencode.ANTIGRAVITY_CLI_AUTORUN=0— disables auto-run whencli.tsis imported as a module.NO_COLOR— disables ANSI colors in CLI output.OPENCODE_IMAGE_ASPECT_RATIO— overrides the image-generation aspect ratio.
Register maene in opencode.json (project-local or ~/.config/opencode/opencode.json). The reference is name-agnostic — any of the forms below works and opencode auto-downloads npm references into its plugin cache (~/.cache/opencode/packages/...):
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@wenathlan/maene"]
}Pick whichever form you prefer — none is forced:
{ "plugin": ["@wenathlan/maene"] }
{ "plugin": ["@wenathlan/maene@latest"] }
{ "plugin": ["@wenathlan/maene@2.1.16"] }
{ "plugin": ["maene@npm:@wenathlan/maene@latest"] }
{ "plugin": ["file:/absolute/path/to/maene"] }The fourth entry is the standard npm alias — it installs the package under the plain name maene in opencode's plugin cache. It exists because npmjs.org permanently refuses to register a package literally named maene (typosquat-similarity with madge/panene/sane — 403 "Package name too similar", verified 2026-08-26), so {"plugin": ["maene"]} cannot resolve; the alias is the supported scope-free spelling.
Or generate the whole model catalog (93 google models, 29 antigravity models, 37 top-level shortcuts) with the CLI:
maene configure --global # writes provider.google.models + provider.antigravity.modelsRun opencode auth login and pick any of the maene entries — they are the plugin acting behind the scenes:
- Google — the standard Google entry. Selecting it offers Login with Google (Antigravity – Gemini CLI bypass), the maene OAuth method (Antigravity CLI masquerade, PKCE, automatic loopback callback — no code copy/paste).
- Antigravity — the standard Antigravity entry with the same OAuth method. maene always ensures a provider
antigravity(npm@ai-sdk/google+ the maene model catalog) exists in memory — unless you defined your ownantigravityblock (yours always wins) or disabled the provider — so this entry is usable with zero config and credentials stored underantigravityalways find their provider. The injection is add-only and never touches your file on disk (opt out with["maene", { "inject_antigravity_provider": false }]— note the npm-alias spelling caveat: with a custom provider configured, disabling the injection means the plainantigravityid must exist in your config before logging in through that entry). - Your custom provider — whatever you named the provider that carries
antigravity-*/gemini-*model ids (casasbahia,my-google, ...) appears with its configured name, recognized by MODEL IDS, never by a fixed name.
The exchanged credential is stored under the provider that actually carries the maene models (model-id detection first, the picked entry's provider id as fallback), so streaming works no matter which entry you picked: auth.loader supplies the active account credentials (auto-refreshed, identity-aware) merged straight into the provider SDK options.
The provider block itself is yours to shape: the npm may be @ai-sdk/google OR @ai-sdk/openai-compatible (both verified live — the loader injects baseURL, the OAuth apiKey, the identity headers and the fetch translator either way, so an openai-compatible block needs no baseURL at all), the provider name is irrelevant (model ids decide), and the same plugin array happily carries other npm plugins next to maene. A real-world example of the verified shape:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@wenathlan/maene@latest", "@zilliz/memsearch-opencode", "opencode-universal-memory"],
"provider": {
"google": {
"name": "google",
"npm": "@ai-sdk/openai-compatible",
"options": {},
"models": { "antigravity-gemini-3-flash": {}, "gemini-2.5-pro": {} }
}
}
}OpenCode >= 1.18 resolves exports["./server"] (server.ts) first; that entry registers the three auth entries above from the single plugin reference. Loaders that read main (plugin.ts) directly keep working through the V1 module shape { id: "maene", server }, which always carries one auth entry (detected provider, google fallback). The hooks object provides:
auth— provider entry with oneoauthmethod:authorize()starts the PKCE local-callback flow (the Antigravity CLI masquerade client) and returns the Google consent URL that opencode renders; after the account owner signs in and grants the consent, the returnedcallback()resolves with the exchanged refresh/access tokens and persists the account in the robin-hood store.auth.loadersupplies the active account credentials (auto-refreshed) for provider requests.config— the documented injection point opencode runs before readingcfg.provider; used only for the zero-config antigravity provider.chat.headers— injects the Gemini CLI identification (User-AgentGeminiCLI/0.57.0,X-Goog-Api-Clientgl-node/22.19.0 gccl/0.9.2,Client-MetadataideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI) on requests from MAENE providers only (the detected provider plus the standardgoogle/antigravityentries — since 2.1.13; model-id substrings alone no longer qualify, soopenai/gpt-oss-120bunder nvidia/openrouter is never polluted) — the dual-bypass second face.tool.execute.before/tool.execute.after— instrumentation passthrough.
The legacy library shape (getPlugin(), the plugin definition object with hooks/auth/config/models) remains exported for direct consumers.
index.ts is the library-first ESM barrel. Import the default plugin or named helpers:
// plugin root (what opencode loads): the plugin definition + core exports
import plugin, { getPlugin, VERSION, CLIENT_ID } from "@wenathlan/maene";
// full barrel (aggregates, namespaces, all helpers)
import {
GEMINI_CLI_BYPASS,
ANTIGRAVITY_CLI_BYPASS,
ALL_MODELS_2026,
MODEL_BY_ID,
MODEL_ROUTING,
} from "@wenathlan/maene/index";
// or namespace imports
import * as Maene from "@wenathlan/maene/index";
const mgr = new Maene.Accounts.AccountManager();
await mgr.load();
const acc = mgr.getNext("round-robin", 90);
console.log(acc?.email);Top-level named exports:
| Export | Purpose |
|---|---|
plugin (default and named) |
The OpenCode plugin definition object. |
getPlugin(options?) |
Singleton factory returning the AntigravityPlugin instance. |
VERSION |
maene version string (matches package.json). |
PLUGIN_ID |
The constant string "maene". |
GEMINI_CLI_BYPASS |
Frozen aggregate of the Gemini CLI identification (clientId, clientSecret, userAgent, xGoogApiClient, clientMetadata, fallbackProject, endpoints, isolatedDir). |
ANTIGRAVITY_CLI_BYPASS |
Frozen aggregate of the official Antigravity CLI (agy) identification — clientId, clientSecret, userAgent, xGoogApiClient, official redirect URI, scopes, PKCE flag. This is the client new logins present since 2.1.8. |
ALL_MODELS_2026 |
Headline 2026 model catalog (rich ModelDefinition[]). |
MODEL_BY_ID |
Lookup record by model id. |
MODEL_ROUTING |
Routing config record (per model group, endpoint order, sandbox skip flag). |
The 21 namespace re-exports from index.ts:
| Namespace | Module | Purpose |
|---|---|---|
Constants |
constants.ts |
L0 raw-value owner (v2.1.15): every cross-domain string/number/map declared once — base URLs, ENDPOINTS host map, user agents, timeouts, version pools. |
Fingerprint |
fingerprint.ts |
L2 identity-masquerade owner (v2.1.15: absorbed the UA/client-metadata/header builders) — weighted platform pools, FNV-1a session ids, getDynamicUserAgent, getXGoogApiClient. |
OAuth |
auth.ts |
v2.1.14 alias: the full OAuth 2.0 PKCE flow (callback server, token refresh, userinfo) now lives in the consolidated authentication module. |
Accounts |
accounts.ts |
L3 account-store owner (v2.1.15: owns the Account type and RotationStrategy): rich v3 AccountManager, store I/O, atomic chmod-0600 writes. |
Auth |
auth.ts |
L2 THE authentication module (v2.1.15: every OAuth client identity — the gemini pair declared once as GEMINI_CLI_OAUTH_CLIENT_ID/SECRET; project discovery lives in project.ts): accountManager, robin-hood getNext, full OAuth flow. |
Core |
core.ts |
L1 generic-utility leaf (v2.1.15: hashing, jitter, fetchWithTimeout, retry classifiers, id generators, LRU thinking cache — schema-cleaner moved to request.ts, transformers to streaming.ts). |
System |
system.ts |
v2.1.14 shim: re-exports the configuration module's system utilities (debug logger, version fetcher, validators, atomic config write). |
Models / Models2026 |
models.ts |
L1 THE model-identification owner (v2.1.15: catalogs, classifiers, thinking tables, routing orders — model names never appear in any other file). |
Project |
project.ts |
L3 THE project-discovery owner (v2.1.15: loadCodeAssist plus onboardUser cascade, projectId cache, resolveProjectId). |
Quota |
quota.ts |
L4 quota owner: dual-source quota manager, adaptive TTL cache, file-backed quota cache, runtime QuotaConfig. |
Config |
config.ts |
L2 THE configuration module (v2.1.15: config + validate + system — schema-driven config (Draft-07), sync and async loaders, validators, config-file IO). |
Debug |
debug.ts |
L1 logging owner: production logger (10 MB rotation, 7-day retention, secret redaction). |
Version |
version.ts |
L2 version-detection owner: dynamic Antigravity version resolver (4-tier legacy plus 4-tier observer chain). |
RequestHelpers |
request.ts |
v2.1.14 alias: schema-cleaner, message normalizer, thinking-budget mapper, variant parser now live in the consolidated request module. |
Request |
request.ts |
L4 THE request-building owner (v2.1.15: the LIVE opencode pipeline): OpenAI/Anthropic to Gemini transform, dual builder, endpoint cascade, tool sanitizers, API_PATHS. |
Streaming |
streaming.ts |
L4 streaming owner: tolerant SSE parser, OpenAI/Anthropic streaming transformers, replay cache, retry. |
Recovery |
recovery.ts |
L4 recovery owner: auto-recovery engine (toast notifier, error detectors, signature preservation, retry wrapper). |
Search |
search.ts |
L4 search-grounding owner: Google Search grounding, urlContext, image-gen config, citation extraction. |
CLI |
cli.ts |
L5 CLI entry (runCLI, subcommand handlers, atomic writers). |
PluginModule |
plugin.ts |
L5 plugin entry, orchestration only (fetch hook, dual-bypass, account selection — every duplicated family imported from its owner). |
package.json subpath exports: ., ./plugin, ./server, ./constants, ./cli, ./auth, ./core, ./models, ./streaming, ./recovery, ./search, ./system, ./antigravity-cli, ./index. The ./system and ./antigravity-cli subpaths resolve to compatibility shims over the consolidated modules (config.ts and auth.ts respectively). All three resolution keys (types, import, default) point at the root .ts file (no build artifact required at dev time).
maene is configured through ~/.config/opencode/antigravity.json (override the location with OPENCODE_CONFIG_DIR). The shipped antigravity.json is dual-purpose: it is both a JSON Schema (Draft-07) for the user-editable keys and a frozen manifest of the Gemini CLI bypass identity (the x-antigravity-bypass object). User keys:
| Key | Type | Default | Purpose |
|---|---|---|---|
oauth_identity |
"auto" or "antigravity-cli" or "gemini-cli" | "auto" | OAuth client identity for NEW logins. auto/antigravity-cli masquerades as the official Antigravity CLI (agy) — its client onboards the current free tier and Google accepts a loopback redirect for it, so the login is fully automatic (no code copy/paste). gemini-cli keeps the legacy client. Stored accounts always refresh with the client they authenticated with. |
cli_first |
boolean | true | Prefer Gemini CLI pool for Gemini models, preserving Antigravity quota for Claude. |
pid_offset_enabled |
boolean | true | 9router PID-offset anti-fingerprint (0 to 1000 ms randomization on user_prompt_id). |
google_search_enabled |
boolean or "auto" or string | false | Inject googleSearch plus urlContext tools. "auto" enables grounding for Gemini models only. |
quota_refresh_interval_minutes |
number 1 to 1440 | 15 | Quota cache refresh interval. |
soft_quota_cache_ttl_minutes |
number 1 to 1440 or "auto" | 5 | Soft-quota cooldown TTL. |
soft_quota_threshold_percent |
number 0 to 100 | 90 | Robin-hood soft-quota rotation threshold. |
rotation_strategy |
"round-robin" or "sticky" | "round-robin" | Account selection strategy. |
claude_tool_hardening |
boolean | true | Prefix Claude tool descriptions with [hardening] hint. |
keep_thinking |
boolean | false | Preserve thinking blocks across turns. |
toast_scope |
"all" or "project" or "minimal" or "none" or "root_only" | "all" | Toast notification scope. |
quota_fallback |
boolean or "auto" or "antigravity-first" or "cli-first" | "antigravity-first" | Quota-exhausted fallback policy. |
quiet_mode |
boolean | false | Suppress non-error toasts. |
debug |
boolean or object | false | Safe verbose logs (no secret exposure). Object form: { level, log_file, retain_days, max_bytes, tui_buffer_lines, redact_secrets }. |
version_cache_ttl |
number 1 to 10080 | 60 | Dynamic Antigravity version cache TTL in minutes. |
endpoints |
object | omitted | Per-tier endpoint overrides: { prod, daily, sandbox, autopush }. |
Minimal example:
{
"cli_first": true,
"google_search_enabled": "auto",
"soft_quota_threshold_percent": 90,
"rotation_strategy": "round-robin",
"claude_tool_hardening": true,
"keep_thinking": false
}Validation: antigravity.schema.json is the strict standalone schema (Draft-07, $id https://opencode.ai/schemas/antigravity.json, additionalProperties: false). The in-code schema (config.ts ANTIGRAVITY_CONFIG_JSON_SCHEMA) is a third superset that unifies both files. The bypass identity itself (Gemini CLI client id, secret, UA, headers, fallback project) is shipped-frozen and is not user-editable.
The bypass has two faces that share the same JSON identification:
-
Antigravity bypass (
buildAntigravityRequestinplugin.tsandcore.ts): wraps the OpenCode request as{ project: "projects/<id>", model, request: { model, contents, tools, generationConfig } }and POSTs tohttps://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=ssewith antigravity-style headers (User-Agent: antigravity/<v> <os>/<arch>,Authorization: Bearer <token>). Cascade-fallbacks todaily-cloudcode-pa.googleapis.comthendaily-cloudcode-pa.sandbox.googleapis.com(andautopush-cloudcode-pa.sandbox.googleapis.com) on 403, 404, or 5xx, with 0 to 80 ms jitter. Forbidden headers are stripped on every request:x-goog-user-project,x-goog-quota-user,x-client-device-id,x-goog-request-reason. -
Gemini CLI identification spoof (
buildGeminiCLIRequestinplugin.tsandcore.ts): same envelope shape, but headers carry the exact Gemini CLI identification (User-Agent: GeminiCLI/0.57.0,X-Goog-Api-Client: gl-node/22.19.0,Client-Metadata: ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI). Used when the model isgemini-cli-only (gemini-3, gemini-3.1, gemini-3.5, gemini-3.6, gemini-3.7, preview, customtools variants) or whencli_first:trueand the model is a Gemini. Sandbox endpoints are skipped for these models. -
Antigravity CLI client (identity bundle in the consolidated
auth.ts, exposed via theantigravity-cli.tsshim; default OAuth identity for NEW logins since 2.1.8): maene authenticates with the exact OAuth client of the official Antigravity CLI (agy) — consent screen renders as "Google Antigravity", PKCE S256, scopes includeaicode/cclog/experimentsandconfigs, and the login uses a loopback redirect Google accepts for this client, making the whole flow automatic (the official CLI shows a code to copy/paste; maene captures the authorization on127.0.0.1directly — no copy/paste ever). Request headers for these accounts:User-Agent: antigravity-cli/1.1.21 (<os>; <arch>),X-Goog-Api-Client: antigravity-cli/1.1.21 grpc-go/1.85.0.
All faces share the resolveProjectId cascade: if no project id is cached for the access token, call loadCodeAssist (metadata-only, no project field). If null, onboardUser with tierId: "FREE" (long-running op polled up to 5 times with backoff). Retry loadCodeAssist. If still null, fall back to rising-fact-p41fc. Project ids are cached in-memory keyed by a SHA-256-truncated access token, so a heap snapshot cannot leak the raw token. maene never asks the user for a GCP project.
Implemented in auth.ts accountManager.getNext() (with a lightweight InlineAccountManager.getNextAccount fallback in plugin.ts, and a richer AccountManager class in accounts.ts). The account store format is v3:
Each accountEntry carries: email, refreshToken, accessToken, expiry, createdAt, addedAt, lastUsed, projectId, managedProjectId, disabled, quotaExhaustedUntil, softQuotaUntil, remaining, limit, rateLimitResetTimes (a per-model reset-time map).
getNext(strategy, softThreshold=90) filters accounts by, in order:
- Not disabled.
quotaExhaustedUntilexpired.softQuotaUntilexpired.- All
rateLimitResetTimesfor the current model expired. - Soft-quota filter — used percentage
((limit - remaining) / limit) * 100 < softThreshold(90 percent by default).
If the filtered pool is non-empty, it picks accounts[idx % pool.length] (round-robin) or the sticky account, advances idx, and persists activeIndex. If empty, it falls back to the not-rate-limited pool, then to the full active pool. setRateLimit(email, modelKey, resetMs) is called when cloudcode-pa returns a Retry-After or X-RateLimit-Reset. markExhausted (1 hour default) and markSoftQuota (15 minutes default, configurable via soft_quota_cache_ttl_minutes) implement the dual-quota cooldowns. The plugin's getAccountForModel further applies cli_first dual-quota routing: if the model is Gemini and cli_first:true, prefer a gemini-cli pool account so the Antigravity pool is preserved for Claude calls.
Quota is fetched in parallel from both Antigravity and Gemini CLI sources by quota.ts retrieveDualQuota. The result is cached through an adaptive TTL (2 to 30 minutes, based on fill ratio) backed by a file at ~/.config/opencode/cache/antigravity/quota.json (atomic chmod-0600 write). Soft threshold is 90 percent; hard threshold is 98 percent.
Root-first flat layout (no /src). Since v2.1.15 the root follows a strict layer DAG — every logic family has exactly one owner file and synchronization between files is import dependency, never copy. Runtime imports point downward only (a lower layer never imports a higher one; import type may cross layers because it erases at runtime):
L0 constants.ts raw shared values only (strings, numbers, host maps, pools)
L1 core.ts generic pure utilities (hash, jitter, retry, ids, timeouts)
L1 models.ts ALL model identification (catalogs, classifiers, routing)
L1 debug.ts logging and redaction
L2 fingerprint.ts identity masquerade (user agents, client metadata, headers)
L2 version.ts version detection
L2 config.ts configuration management, validators, config file IO
L2 auth.ts ALL authentication (oauth clients, scopes, urls, flows)
L3 accounts.ts account store and rotation
L3 project.ts project discovery (loadCodeAssist, onboardUser, projectId cache)
L4 request.ts request building (transforms, builders, cascade, header stripping)
L4 streaming.ts response streaming (sse parse, openai/anthropic transforms)
L4 recovery.ts recovery (toast, error types, signature preservation)
L4 quota.ts quota
L4 search.ts search grounding
L5 plugin.ts / cli.ts / server.ts / index.ts entry orchestration only
shim system.ts, antigravity-cli.ts published-subpath re-exports
| Path | Role |
|---|---|
plugin.ts |
L5 plugin entry, orchestration only (default export, fetch hook, dual-bypass, account selection — every duplicated family imported from its owner). |
cli.ts |
L5 bin entry (maene), subcommand dispatch. |
server.ts |
L5 ./server package entry: the three auth registrations for opencode auth login (google, antigravity, detected provider). |
antigravity-cli.ts |
compatibility shim over auth.ts (published ./antigravity-cli subpath). |
index.ts |
L5 ESM barrel (namespace re-exports incl. compat aliases, default plugin, named constants). |
config.ts |
L2 THE configuration module (v2.1.15: config + validate + system — owns validators and config-file IO). |
constants.ts |
L0 THE raw-value owner (v2.1.15: every cross-domain string/number/map lives here once — base URLs, ENDPOINTS host map, user agents, timeouts, version pools). |
models.ts |
L1 THE model-identification owner (v2.1.15: catalogs, classifiers, thinking tables, endpoint routing orders — model names never appear in any other file). |
accounts.ts |
L3 THE account-store owner (v3 AccountManager, store I/O, rotation, Account type). |
auth.ts |
L2 THE authentication module (v2.1.15: every OAuth client identity, PKCE flow, callback server, token refresh, userinfo — the gemini client pair declared once as GEMINI_CLI_OAUTH_CLIENT_ID/SECRET). |
core.ts |
L1 THE generic-utility leaf (v2.1.15: fnv hashes, jitter, fetchWithTimeout, retry classifiers, id generators, LRU thinking cache — every legacy compact duplicate deleted). |
debug.ts |
L1 THE logging owner (production logger, 10 MB rotation, 7-day retention, secret redaction). |
fingerprint.ts |
L2 THE identity-masquerade owner (v2.1.15: getDynamicUserAgent, getXGoogApiClient, buildClientMetadata, buildGeminiUserAgent, gemini/antigravity header builders, weighted platform pools). |
project.ts |
L3 THE project-discovery owner (v2.1.15: loadCodeAssist, onboardUser, projectId cache, resolveProjectId). |
quota.ts |
L4 THE quota owner (dual-source manager, adaptive TTL cache, runtime QuotaConfig). |
recovery.ts |
L4 THE recovery owner (toast, error detectors, signature preservation, retry wrapper). |
request.ts |
L4 THE request-building owner (v2.1.15: the LIVE opencode pipeline — transforms, dual builders, fetchWithCascade, tool sanitizers, API_PATHS). |
search.ts |
L4 THE search-grounding owner (googleSearch, urlContext, image-gen config, citations). |
streaming.ts |
L4 THE streaming owner (tolerant SSE parser, OpenAI/Anthropic streaming transformers, replay cache). |
system.ts |
compatibility shim over config.ts (published ./system subpath). |
version.ts |
L2 THE version-detection owner (dynamic resolver: legacy plus observer chains). |
antigravity.json |
Dual-purpose schema plus bypass manifest. |
antigravity.schema.json |
Strict standalone JSON Schema (Draft-07). |
tests/ |
node:test plus node:assert suites (constants, config validators, system shim, auth PKCE, fingerprint FNV-1a-32, models 9router mapping). |
docs/ |
Reference docs: README, RESEARCH, WAVES, MODELS, BYPASS, FLUXOGRAMA, CHECKLIST. |
.github/workflows/ |
The eight release/CI pipeline definitions (see Releases and CI). |
SECURITY.md |
Vulnerability reporting policy for the maintained 2.x line. |
bun install # install dev deps
bun run typecheck # tsc --noEmit --skipLibCheck
bun run lint # biome check .
bun run lint:fix # biome check . --write
bun run format # biome format . --write
bun run build # tsup plugin.ts constants.ts --format esm,cjs --clean --splitting=false
bun run pack:check # npm pack --dry-run --json (verify published file list)
bun test # node --test tests/The CI workflow runs bun install --frozen-lockfile, bun run typecheck, bun run lint, bun test, bun run build plus npm pack --dry-run on every push and pull request. Node 26.7.0 is the CI runtime; bun 1.4.0 is the package manager.
Current release: 2.1.9 (26 Aug 2026). See CHANGELOG.md for the full history.
GitHub Actions definitions live directly under .github/workflows/ — the e2ugh layout contract: no nested .github/actions composite folder, every pipeline is a single self-contained workflow file (the workflowlint.yml gate asserts the contract). All the build, pack and publish computation happens on the GitHub Actions runners.
ci.yml— five gates on everypushtomainand everypull_request: Biome 2.5.10 lint, TypeScript 7.0.2 typecheck (tsc --noEmit), thenode:testsuite through bun, the tsup production bundle plusnpm pack --dry-runvalidation, and strict JSON validation of every tracked document. Concurrency cancels superseded runs on the same ref.release.yml— the reference release scheme: ametadatajob resolves the version tag (creating it automatically when a push tomainbumpspackage.json), verifies theindex.tsVERSIONexport and theCHANGELOG.mdsection agree with the release, arelease-artifactsjob builds a maximum-compressionzip -9source archive of the tagged tree plus SHA-256 checksums, and thegithub-releasejob publishes the release throughsoftprops/action-gh-release@v3.0.2with the curated notes extracted fromCHANGELOG.mdand auto-generated commit listing.publishnpmjs.yml— publishes@wenathlan/maeneto npmjs.com. Fires onrelease: [published], on theworkflow_runcompletion of the Release workflow (the GITHUB_TOKEN event-suppression escape hatch) and onworkflow_dispatchwith ataginput. Aresolvejob validates the tag against the on-diskpackage.jsonand the GitHub release list (untrusted-input hardening: event data is never interpolated into checkout refs); thepublishjob runs typecheck, lint, tests andnpm pack --dry-runinline, normalizes theNPM_TOKENsecret (strips CR/LF), runsnpm whoami, and publishes with skip-if-version-exists. When the secret is absent the job skips with a notice instead of failing the pipeline.publishgithubnpm.yml— the same two-job scheme publishing@wenathlan/maeneto the GitHub npm registry (npm.pkg.github.com) with the job token (packages: write), scope-aligned to the repository owner, with skip-if-version-exists against the GitHub registry.cachecleanup.yml— aggressive cache retention on a twice-daily schedule (03:17 and 15:17 UTC) plus after the Security workflow: exactly one most-recently-accessed entry per cache family (bun, codeql) onmain, everything deleted on non-default refs. Manual dispatch runs in dry-run mode unlessapply=trueis passed.workflowlint.yml— actionlint syntax and policy lint viareviewdog/action-actionlint@v1.73.2on every workflow change, plus the layout assertion that.github/actions/does not exist.security.yml— CodeQL analysis (javascript-typescriptandactionslanguages),dependency-reviewon pull requests, TruffleHog secret scan of the full git history with--results=verified, and the OSSF Scorecard supply-chain analysis published to the public API. Runs on pushes, PRs and weekly.securitypolicy.yml— assertsSECURITY.mddocuments the supported2.xline in lockstep with the shipped package version.
Required secret: NPM_TOKEN — an automation-scoped npm access token with publish rights on @wenathlan/maene. Configure under Settings, Secrets and variables, Actions. The GitHub Packages publication needs no secret (it uses the job token).
To publish manually (skip the release workflow):
npm publish --access public # npmjs.com
npm publish --registry=https://npm.pkg.github.com --access public # GitHub Packages- Fork the repository at
https://github.com/wenathlan/maene. - Create a feature branch from
main. bun install, write code, add tests undertests/, runbun test.bun run lintandbun run typecheckmust pass.- Open a pull request against
main. Conventional commits preferred (feat:,fix:,docs:,chore:,refactor:,test:).
MIT — see LICENSE.
- npm:
https://www.npmjs.com/package/@wenathlan/maene - GitHub:
https://github.com/wenathlan/maene - OpenCode:
https://github.com/opencode-ai
{ "version": 3, "accounts": [ /* accountEntry objects */ ], "activeIndex": 0, "activeIndexByFamily": { "claude": 0, "gemini": 0 } }