Guidance for AI agents working with this repository.
OpenCode plugin for Google Antigravity OAuth. Intercepts fetch() calls to generativelanguage.googleapis.com, transforms them to Antigravity format, and handles auth, quota, recovery, and multi-account rotation.
bun install # Install dependencies
bun run build # Compile (tsc -p tsconfig.build.json)
bun run typecheck # Type-check only (tsc --noEmit)
bun run test # Run all unit tests (bun test --isolate across packages)
bun test --isolate src/plugin/auth.test.ts # Single test file
bun test --isolate -t "test name here" # Single test by name (uses bun:test's -t filter)
bun test --isolate --watch src/plugin/auth.test.ts # Watch mode, single file
bun run test:e2e # Deterministic e2e flows (mock Antigravity server, no network)
bun run test:e2e:models # Live Antigravity model inventory (CI-gated, network)
bun run test:e2e:regression # Cross-model + Gemini CLI regression (network)
bun run --cwd packages/opencode smoke:tui # Pack-and-install smoke test for the TUI subpathFormat and lint via Biome:
bun run format # biome format --write .
bun run format:check # biome format . (CI gate)
bun run lint # biome lint . (CI gate)Pre-commit hooks (.lefthook.yml) run Biome on changed *.{ts,tsx,js,mjs,json,jsonc,yml,yaml} files via bunx biome check --staged --no-errors-on-unmatched. The repo pins the toolchain in mise.toml (bun = "1.3", node = "24") and CI uses oven-sh/setup-bun@v2 with bun-version: 1.3.
strict: truewith extra strictness:noUncheckedIndexedAccess,noImplicitOverride,noFallthroughCasesInSwitchverbatimModuleSyntax: true— useimport typefor type-only importstarget: ESNext,module: Preserve,moduleResolution: bundlerallowImportingTsExtensions: true— use.tsextensions in imports- No path aliases — all imports are relative
- Use
import type { ... }for type-only imports (enforced byverbatimModuleSyntax) - Named imports only — no default imports in src/
- Relative paths with
.tsextensions:import { foo } from "./bar.ts" - Order: node builtins > external packages > local modules
- Named exports only in src/ — no default exports
- Barrel files (index.ts) for module surfaces
camelCasefor functions, variables, parametersPascalCasefor types, interfaces, classes, enumsUPPER_SNAKE_CASEfor constantskebab-casefor file names (e.g.,request-helpers.ts,thinking-recovery.ts)- Test files:
*.test.tscolocated with source
- No
Iprefix on interfaces, noTypesuffix - Use
z.infer<typeof Schema>for Zod-derived types - Extract to
types.tswhen shared, inline when local - Discriminated unions preferred over boolean flags
- Never use
as any,@ts-ignore, or@ts-expect-error
export functionfor public APIs- Arrow functions for callbacks, factories, and inline closures
- Async functions with targeted try/catch (not blanket)
- Defensive try/catch with graceful degradation (fallback values, not crashes)
- Custom error classes with metadata when domain-specific
- Catch
unknown, log, and convert to domain errors — never empty catch blocks - Rate limit / quota errors trigger account rotation, not failure
- 2-space indentation
- Single quotes for strings (Biome
quoteStyle: single) - Trailing commas in multiline constructs
- No semicolons (project convention)
createLogger("module-name")for structured loggingconsole.logonly for CLI/user-facing output
The repository is a Bun workspace with three packages. The pre-monorepo
single-root-src/ layout was retired when the project split into
packages/{core,opencode,pi} so the harness-agnostic engine, the host
adapter, and the Pi host can ship on independent cadences.
packages/
├── core/ # Harness-agnostic engine — auth, transform, storage, fingerprinting
│ └── src/
│ ├── index.ts # Public barrel
│ ├── account-manager.ts # Per-account selection, rate-limit, quota, fingerprint
│ ├── account-storage.ts # v4 schema + lock-held read-modify-write + fail-closed unreadable
│ ├── agy-transport.ts # Bounded TLS pool, gzip/chunk decode, idle watchdog
│ ├── agy-request-metadata.ts
│ ├── antigravity/oauth.ts # OAuth token exchange + refresh
│ ├── auth.ts # Token validation helpers
│ ├── file-lock.ts # Fenced file lock for concurrent writes
│ ├── fingerprint.ts # Device fingerprint construction
│ ├── model-registry.ts # Anthropic / Gemini / GPT-OSS model definitions
│ ├── project.ts # Managed project resolution
│ ├── quota-manager.ts # Quota caching + fallbacks
│ ├── rotation.ts # Account rotation state
│ └── transform/ # Claude / Gemini / cross-model sanitizer + tests
├── opencode/ # OpenCode host adapter — plugin entry, fetch interceptor, TUI
│ └── src/
│ ├── cli.ts # `antigravity-auth` CLI (login / list / quota)
│ ├── plugin/ # Plugin factory, OAuth methods, account access, fetch
│ │ ├── fetch-interceptor.ts # Outer/inner loop, retry/quota/routing pipeline
│ │ ├── auth-loader.ts # Host `auth.loader()` integration
│ │ ├── oauth-methods.ts # OAuth menu + callbacks
│ │ ├── persist-account-pool.ts # Lock-held append after fresh OAuth login
│ │ ├── storage.ts # Host-path adapter for account pool (re-exports core errors)
│ │ └── ui/ # TUI sidebar, auth menu, quota-status, command dialogs
│ ├── tui/ # Precompiled + raw TUI sources
│ ├── rpc/ # Loopback RPC server + client (sidebar notifications)
│ └── hooks/ # Lifecycle hooks (auto-update checker, etc.)
└── pi/ # Pi host adapter — thinner facade around core
└── src/
├── index.ts # Public barrel — `streamCortexKitAntigravity` + OAuth helpers
├── convert.ts # Pi <-> Antigravity message-shape conversion
├── credential-cache.ts # Packed refresh token cache
├── paths.ts # Pi AGENT_DIR + account-pool path resolution
└── stream.ts # Stream factory consumed by `index.ts`
Plugin intercepts fetch() for generativelanguage.googleapis.com, transforms to Antigravity format. Two header styles: antigravity (Electron-style UA + fingerprint) and gemini-cli (nodejs-client UA).
ALL thinking blocks are stripped from outgoing requests for Claude models. Claude generates fresh thinking each turn. This eliminates signature validation errors.
When tool execution is interrupted (ESC/timeout), the plugin injects synthetic tool_result blocks to recover the session without starting over.
Tool schemas are cleaned via allowlist. Unsupported fields (const, $ref, $defs) are removed or converted to Antigravity-compatible format.
Accounts rotate on rate limits. Gemini has dual quota pools (Antigravity headers + Gemini CLI headers). Fingerprints are per-account and regenerated on capacity exhaustion.
Per-account device fingerprints stored in antigravity-accounts.json. Each fingerprint includes deviceId, sessionToken, userAgent, and a reduced clientMetadata (ideType, platform, pluginType — no osVersion, arch, or sqmId). The only header composed is User-Agent, built by buildFingerprintHeaders() in fingerprint.ts and applied on the antigravity request path in request.ts. History tracked (max 5), restorable.
zod ^4— schema validation (NOT zod v3)@opencode-ai/plugin— OpenCode plugin interface@openauthjs/openauth— OAuth clientproper-lockfile— file locking for concurrent accessxdg-basedir— XDG directory resolution
- Framework: Bun's test runner (
bun test; thebun:testmodule re-exports a jest-compatible namespace) - Config: no per-package config — Bun discovers
*.test.tsnext to source. Thebunfig.tomlat the root and in each workspace preloadstest/setup.ts(env-isolation + a per-test mkdtemp root + aglobalThis.stubbed/unstubAllGlobals/freshImporthelper). The e2e workspace usesbunfig.tomlroot = "./src"so its tests stay isolated from the unit workspace. - Tests colocated:
src/plugin/foo.test.tsnext tosrc/plugin/foo.ts. The e2e workspace uses*.e2e.test.tsso the rootbun run testandbun run test:e2eselectors can target them precisely. - Use
describe/it/expectfrombun:test— the standard API. - Mock with
mock(),spyOn(), and thejestnamespace exported frombun:test(jest.fn,jest.spyOn,jest.setSystemTime, etc.). Thetest/setup.tspreload patchesjest.setSystemTime/jest.useRealTimersso they also spy onDate.now()(Bun'sbun:testclock only fakes the timer queue, not the wall clock).
- README.md — Installation & usage
- docs/ARCHITECTURE.md — Detailed architecture guide
- docs/ANTIGRAVITY_API_SPEC.md — API reference
- CHANGELOG.md — Version history
- ARCHITECTURE.md and STRUCTURE.md — repo-level architecture and file-system map
- biome.json — formatter + linter config
- lefthook.yml — pre-commit Biome hook
- mise.toml — pinned
bun+nodeversions