Showing preview only (1,395K chars total). Download the full file or copy to clipboard to get everything.
Repository: NoeFabris/opencode-antigravity-auth
Branch: main
Commit: 09ccf4bbfe1a
Files: 127
Total size: 1.3 MB
Directory structure:
gitextract_q9qv3hab/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ └── workflows/
│ ├── ci.yml
│ ├── issue-triage.yml
│ ├── release-beta.yml
│ ├── release.yml
│ ├── republish-version.yml
│ └── update-dist-tag.yml
├── .gitignore
├── AGENTS.MD
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets/
│ └── antigravity.schema.json
├── docs/
│ ├── ANTIGRAVITY_API_SPEC.md
│ ├── ARCHITECTURE.md
│ ├── CONFIGURATION.md
│ ├── MODEL-VARIANTS.md
│ ├── MULTI-ACCOUNT.md
│ └── TROUBLESHOOTING.md
├── index.ts
├── package.json
├── script/
│ ├── build-schema.ts
│ ├── test-cross-model-e2e.sh
│ ├── test-cross-model.ts
│ ├── test-gemini-cli-e2e.sh
│ ├── test-models.ts
│ └── test-regression.ts
├── scripts/
│ ├── README-PI.md
│ ├── auth-pi-tools.sh
│ ├── check-quota.mjs
│ ├── setup-opencode-pi.sh
│ └── setup-pi-runner.sh
├── src/
│ ├── antigravity/
│ │ └── oauth.ts
│ ├── constants.test.ts
│ ├── constants.ts
│ ├── hooks/
│ │ └── auto-update-checker/
│ │ ├── cache.ts
│ │ ├── checker.ts
│ │ ├── constants.ts
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ ├── logging.ts
│ │ └── types.ts
│ ├── plugin/
│ │ ├── accounts.test.ts
│ │ ├── accounts.ts
│ │ ├── antigravity-first-fallback.test.ts
│ │ ├── auth.test.ts
│ │ ├── auth.ts
│ │ ├── cache/
│ │ │ ├── index.ts
│ │ │ └── signature-cache.ts
│ │ ├── cache.test.ts
│ │ ├── cache.ts
│ │ ├── cli.ts
│ │ ├── config/
│ │ │ ├── index.ts
│ │ │ ├── loader.ts
│ │ │ ├── models.test.ts
│ │ │ ├── models.ts
│ │ │ ├── schema.test.ts
│ │ │ ├── schema.ts
│ │ │ ├── updater.test.ts
│ │ │ └── updater.ts
│ │ ├── core/
│ │ │ └── streaming/
│ │ │ ├── index.ts
│ │ │ ├── transformer.ts
│ │ │ └── types.ts
│ │ ├── cross-model-integration.test.ts
│ │ ├── debug.test.ts
│ │ ├── debug.ts
│ │ ├── errors.ts
│ │ ├── fingerprint.ts
│ │ ├── image-saver.ts
│ │ ├── logger.test.ts
│ │ ├── logger.ts
│ │ ├── logging-utils.test.ts
│ │ ├── logging-utils.ts
│ │ ├── model-specific-quota.test.ts
│ │ ├── persist-account-pool.test.ts
│ │ ├── project.ts
│ │ ├── quota-fallback.test.ts
│ │ ├── quota.ts
│ │ ├── recovery/
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── storage.ts
│ │ │ └── types.ts
│ │ ├── recovery.test.ts
│ │ ├── recovery.ts
│ │ ├── refresh-queue.test.ts
│ │ ├── refresh-queue.ts
│ │ ├── request-helpers.test.ts
│ │ ├── request-helpers.ts
│ │ ├── request.test.ts
│ │ ├── request.ts
│ │ ├── rotation.test.ts
│ │ ├── rotation.ts
│ │ ├── search.ts
│ │ ├── server.ts
│ │ ├── storage.test.ts
│ │ ├── storage.ts
│ │ ├── stores/
│ │ │ └── signature-store.ts
│ │ ├── thinking-recovery.ts
│ │ ├── token.test.ts
│ │ ├── token.ts
│ │ ├── transform/
│ │ │ ├── claude.test.ts
│ │ │ ├── claude.ts
│ │ │ ├── cross-model-sanitizer.test.ts
│ │ │ ├── cross-model-sanitizer.ts
│ │ │ ├── gemini.test.ts
│ │ │ ├── gemini.ts
│ │ │ ├── index.ts
│ │ │ ├── model-resolver.test.ts
│ │ │ ├── model-resolver.ts
│ │ │ └── types.ts
│ │ ├── types.ts
│ │ ├── ui/
│ │ │ ├── ansi.test.ts
│ │ │ ├── ansi.ts
│ │ │ ├── auth-menu.test.ts
│ │ │ ├── auth-menu.ts
│ │ │ ├── confirm.ts
│ │ │ └── select.ts
│ │ ├── version.test.ts
│ │ └── version.ts
│ ├── plugin.ts
│ └── shims.d.ts
├── tsconfig.build.json
├── tsconfig.json
└── vitest.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: NoeFabris
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: noefabris
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: Report a bug or issue with the plugin
labels: ["bug", "needs-triage"]
body:
- type: markdown
attributes:
value: |
## Before opening an issue
> ⚠️ **PLEASE READ THE TROUBLESHOOTING GUIDE FIRST**
>
> Most issues (rate limits, hanging prompts, auth errors) are covered in our troubleshooting guide.
> **Your issue may already have a solution!**
**Please write a descriptive title above** (e.g., "Auth fails with invalid_grant after token refresh")
> 🌐 **ENGLISH ONLY**
>
> Issues must be written in English. Issues in other languages will be closed immediately.
Check the following resources first:
- **[📋 Troubleshooting Guide](https://github.com/NoeFabris/opencode-antigravity-auth/blob/main/docs/TROUBLESHOOTING.md)** - common issues and solutions
- [Existing issues](https://github.com/NoeFabris/opencode-antigravity-auth/issues?q=is%3Aissue) - your issue may already be reported
**Issues without debug logs will be closed.**
- type: checkboxes
id: prerequisites
attributes:
label: Pre-submission checklist
options:
- label: I have searched existing issues for duplicates
required: true
- label: I have read the [📋 Troubleshooting Guide](https://github.com/NoeFabris/opencode-antigravity-auth/blob/main/docs/TROUBLESHOOTING.md)
required: true
- label: I have read the [README](https://github.com/NoeFabris/opencode-antigravity-auth#readme) installation instructions
- type: dropdown
id: model
attributes:
label: Model used
description: Which model were you using when the issue occurred?
options:
- antigravity-gemini-3-pro
- antigravity-gemini-3-flash
- antigravity-claude-sonnet-4-6
- antigravity-claude-opus-4-6-thinking
- gemini-2.5-flash
- gemini-2.5-pro
- gemini-3-flash-preview
- gemini-3-pro-preview
- Other (specify in description)
- Not applicable
validations:
required: true
- type: textarea
id: error-message
attributes:
label: Exact error message
description: Copy-paste the exact error message. Do not paraphrase.
placeholder: |
Paste the exact error message here.
Include any error codes or stack traces if available.
render: text
validations:
required: true
- type: textarea
id: description
attributes:
label: Bug description
description: A clear and concise description of what the bug is.
placeholder: Describe the bug...
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: How can we reproduce this issue?
placeholder: |
1. Run command...
2. Click on...
3. See error...
validations:
required: true
- type: dropdown
id: worked-before
attributes:
label: Did this ever work?
description: Has this feature worked for you before?
options:
- First time setup (never worked)
- Worked before, now broken (regression)
- Not sure
validations:
required: true
- type: dropdown
id: num-accounts
attributes:
label: Number of Google accounts configured
options:
- "1"
- "2-3"
- "4+"
validations:
required: true
- type: dropdown
id: reproducibility
attributes:
label: Reproducibility
description: How often does this issue occur?
options:
- Always (100%)
- Often (>50%)
- Sometimes (<50%)
- Once (cannot reproduce)
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Plugin version
description: "Run: `npm list opencode-antigravity-auth -g 2>/dev/null || npm list opencode-antigravity-auth`"
placeholder: "e.g., 1.2.9-beta.5"
validations:
required: true
- type: input
id: opencode-version
attributes:
label: OpenCode version
description: "Run: `opencode --version`"
placeholder: "e.g., 0.3.0"
validations:
required: true
- type: input
id: os
attributes:
label: Operating System
placeholder: "e.g., macOS 15.2, Ubuntu 24.04, Windows 11"
validations:
required: true
- type: input
id: node-version
attributes:
label: Node.js version
description: "Run: `node --version`"
placeholder: "e.g., v22.12.0"
validations:
required: true
- type: dropdown
id: environment
attributes:
label: Environment type
description: Are you running in a special environment?
options:
- Standard (native terminal)
- WSL2
- Docker
- SSH / Remote
- VS Code Remote
- Other (specify in additional context)
validations:
required: true
- type: textarea
id: mcp-servers
attributes:
label: MCP servers installed
description: List any MCP (Model Context Protocol) servers you have configured. This helps us identify potential interactions or conflicts.
placeholder: |
Example:
- chrome-devtools
- figma
validations:
required: false
- type: textarea
id: logs
attributes:
label: Debug logs (REQUIRED)
description: |
**Issues without logs will be closed.**
1. Enable debug logging: `export OPENCODE_ANTIGRAVITY_DEBUG=2`
2. Reproduce the issue
3. Find log file at:
- macOS/Linux: `~/.config/opencode/antigravity-logs/antigravity-debug-<timestamp>.log`
- Windows: `%APPDATA%\opencode\antigravity-logs\antigravity-debug-<timestamp>.log`
4. Paste relevant sections or attach the file
placeholder: Paste your debug logs here...
render: text
validations:
required: true
- type: textarea
id: config
attributes:
label: Configuration (optional)
description: Share your antigravity.json if relevant (remove sensitive data)
placeholder: |
{
"quiet_mode": false,
"debug": false
}
render: json
validations:
required: false
- type: checkboxes
id: compliance
attributes:
label: Compliance
options:
- label: I'm using this plugin for personal development only
required: true
- label: This issue is not related to commercial use or TOS violations
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Any other relevant information
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Troubleshooting Guide
url: https://github.com/NoeFabris/opencode-antigravity-auth#troubleshoot
about: Check the troubleshooting section first - most issues are covered here
- name: Search Existing Issues
url: https://github.com/NoeFabris/opencode-antigravity-auth/issues?q=is%3Aissue
about: Your issue may have already been reported or resolved
- name: Discussions
url: https://github.com/NoeFabris/opencode-antigravity-auth/discussions
about: Ask questions or discuss the plugin with the community
- name: Google Cloud Support
url: https://cloud.google.com/support
about: For Google Cloud account or billing issues, contact Google Cloud directly
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature Request
description: Suggest a new feature or enhancement
labels: ["enhancement", "needs-triage"]
body:
- type: markdown
attributes:
value: |
## Before opening a feature request
**Please write a descriptive title above** (e.g., "Add support for custom retry strategies")
> 🌐 **ENGLISH ONLY**
>
> Issues must be written in English. Issues in other languages will be closed immediately.
Check if this feature has already been requested:
- [Existing feature requests](https://github.com/NoeFabris/opencode-antigravity-auth/issues?q=is%3Aissue+label%3Aenhancement)
- [Discussions](https://github.com/NoeFabris/opencode-antigravity-auth/discussions)
- type: checkboxes
id: prerequisites
attributes:
label: Pre-submission checklist
options:
- label: I have searched existing issues and feature requests for duplicates
required: true
- label: I have read the [README](https://github.com/NoeFabris/opencode-antigravity-auth#readme)
required: true
- type: textarea
id: description
attributes:
label: Feature description
description: A clear description of the feature you'd like to see.
placeholder: Describe the feature...
validations:
required: true
- type: textarea
id: use-case
attributes:
label: Use case
description: Explain how this feature would be used and what problem it solves.
placeholder: |
As a user, I want to...
So that I can...
validations:
required: true
- type: textarea
id: implementation
attributes:
label: Proposed implementation
description: If you have ideas about how this could be implemented, share them here.
placeholder: Optional implementation details...
validations:
required: false
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Have you considered any alternative solutions or workarounds?
placeholder: Other approaches you've thought about...
validations:
required: false
- type: checkboxes
id: compliance
attributes:
label: Compliance
options:
- label: This feature is for personal development use
required: true
- label: This feature does not violate or circumvent Google's Terms of Service
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context, screenshots, or examples.
validations:
required: false
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
name: Test on Node.js
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run type check
run: npm run typecheck
- name: Run tests
run: npm test
- name: Build
run: npm run build
================================================
FILE: .github/workflows/issue-triage.yml
================================================
name: '🏷️ Issue Triage'
on:
issues:
types:
- 'opened'
- 'reopened'
issue_comment:
types:
- 'created'
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to triage'
required: true
type: 'number'
concurrency:
group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number }}'
cancel-in-progress: true
permissions:
contents: 'read'
issues: 'write'
jobs:
triage-issue:
if: |
github.event_name == 'workflow_dispatch' ||
github.event_name == 'issues' ||
(
github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '/triage') &&
(
github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'
)
)
timeout-minutes: 30
runs-on: self-hosted
steps:
- name: 'Check if already triaged'
id: 'check_labels'
if: github.event_name != 'workflow_dispatch'
uses: 'actions/github-script@v7'
with:
script: |
const labels = context.payload.issue?.labels?.map(l => l.name) || [];
const triageLabels = ['bug', 'enhancement', 'documentation', 'question', 'invalid'];
const areaLabels = labels.filter(l => l.startsWith('area/'));
const hasTriageLabel = labels.some(l => triageLabels.includes(l)) || areaLabels.length > 0;
const isRetriage = context.payload.comment?.body?.includes('/triage');
if (hasTriageLabel && !labels.includes('needs-triage') && !isRetriage) {
core.info(`Issue already triaged: ${labels.join(', ')}. Skipping.`);
core.setOutput('skip', 'true');
} else {
core.setOutput('skip', 'false');
}
- name: 'Get issue data'
id: 'get_issue'
if: steps.check_labels.outputs.skip != 'true'
uses: 'actions/github-script@v7'
with:
script: |
const issueNumber = context.payload.inputs?.issue_number || context.issue?.number;
core.setOutput('number', issueNumber);
- name: 'Run Opencode Triage'
id: 'opencode_triage'
if: steps.check_labels.outputs.skip != 'true'
run: |
echo "Running Opencode Triage on issue ${{ steps.get_issue.outputs.number }}..."
# Move to the repository
cd /home/admin/coding/opencode-antigravity-auth
# Run CLI with the agent config
OUTPUT=$(opencode run --agent triage-bot "Triage issue ${{ steps.get_issue.outputs.number }}")
echo "Opencode Output: $OUTPUT"
echo "result<<EOF" >> $GITHUB_OUTPUT
echo "$OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: 'Apply Labels and Respond'
if: steps.check_labels.outputs.skip != 'true'
uses: 'actions/github-script@v7'
env:
ISSUE_NUMBER: '${{ steps.get_issue.outputs.number }}'
OPENCODE_OUTPUT: '${{ steps.opencode_triage.outputs.result }}'
with:
script: |
const rawOutput = process.env.OPENCODE_OUTPUT;
if (!rawOutput) {
core.warning('No triage output available');
return;
}
core.info(`Triage output: ${rawOutput}`);
let parsed;
try {
parsed = JSON.parse(rawOutput.trim());
} catch (e) {
// Try to extract JSON from code blocks first
const codeBlockMatch = rawOutput.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
// Find the LAST JSON object containing triage fields (avoid matching code snippets)
// Match JSON objects that contain "type_label" to ensure we get the triage output
const triageJsonMatch = rawOutput.match(/\{"type_label"[\s\S]*?\}(?=\s*$|\s*```|\s*\n\n)/);
// Fallback: find all potential JSON objects and try parsing from the end
let jsonStr = codeBlockMatch?.[1] || triageJsonMatch?.[0];
if (!jsonStr) {
// Last resort: find all { } pairs and try parsing from the last one
const allMatches = [...rawOutput.matchAll(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g)];
for (let i = allMatches.length - 1; i >= 0; i--) {
try {
const candidate = JSON.parse(allMatches[i][0]);
if (candidate.type_label && candidate.area_label) {
jsonStr = allMatches[i][0];
break;
}
} catch {}
}
}
if (jsonStr) {
try {
parsed = JSON.parse(jsonStr.trim());
} catch (e2) {
core.setFailed(`Failed to parse output: ${rawOutput}`);
return;
}
} else {
core.setFailed(`Failed to parse output: ${rawOutput}`);
return;
}
}
const typeLabel = parsed.type_label;
const areaLabel = parsed.area_label;
const duplicateOf = parsed.duplicate_of;
const suggestedResponse = parsed.suggested_response;
// Validate required fields
if (!typeLabel || !areaLabel || suggestedResponse === undefined) {
core.setFailed(`Invalid JSON structure: missing required fields. Parsed: ${JSON.stringify(parsed)}`);
return;
}
const validTypeLabels = ['bug', 'enhancement', 'documentation', 'question', 'invalid'];
const validAreaLabels = ['area/auth', 'area/models', 'area/config', 'area/compat'];
if (!validTypeLabels.includes(typeLabel)) {
core.warning(`Invalid type_label: ${typeLabel}`);
return;
}
if (!validAreaLabels.includes(areaLabel)) {
core.warning(`Invalid area_label: ${areaLabel}`);
return;
}
let labelsToAdd = [];
let labelsToRemove = ['needs-triage'];
// Remove existing triage labels to prevent conflicts
validTypeLabels.forEach(label => labelsToRemove.push(label));
validAreaLabels.forEach(label => labelsToRemove.push(label));
// Only add one type label and one area label
if (typeLabel && validTypeLabels.includes(typeLabel)) {
labelsToAdd.push(typeLabel);
}
if (areaLabel && validAreaLabels.includes(areaLabel)) {
labelsToAdd.push(areaLabel);
}
if (duplicateOf) {
labelsToAdd.push('duplicate');
}
// Validate single label selection and deduplicate
const typeLabels = labelsToAdd.filter(label => validTypeLabels.includes(label));
const areaLabels = labelsToAdd.filter(label => validAreaLabels.includes(label));
const otherLabels = labelsToAdd.filter(label => !validTypeLabels.includes(label) && !validAreaLabels.includes(label));
if (typeLabels.length > 1) {
core.warning(`Multiple type labels detected: ${typeLabels.join(', ')}. Using only: ${typeLabel}`);
}
if (areaLabels.length > 1) {
core.warning(`Multiple area labels detected: ${areaLabels.join(', ')}. Using only: ${areaLabel}`);
}
// Rebuild labelsToAdd with single type and area labels
labelsToAdd = [];
if (typeLabel && validTypeLabels.includes(typeLabel)) {
labelsToAdd.push(typeLabel);
}
if (areaLabel && validAreaLabels.includes(areaLabel)) {
labelsToAdd.push(areaLabel);
}
labelsToAdd.push(...otherLabels);
// Avoid removing labels that are being added
labelsToRemove = labelsToRemove.filter(label => !labelsToAdd.includes(label));
const issueNumber = parseInt(process.env.ISSUE_NUMBER);
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: labelsToAdd
});
core.info(`Added labels: ${labelsToAdd.join(', ')}`);
for (const label of labelsToRemove) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name: label
});
core.info(`Removed label: ${label}`);
} catch (e) {
core.info(`Label ${label} not present, skipping removal`);
}
}
if (suggestedResponse && suggestedResponse.trim()) {
let body = `👋 Thanks for opening this issue!\n\n${suggestedResponse}`;
if (duplicateOf) {
body += `\n\n🔗 This appears to be related to #${duplicateOf}. Please check that issue for updates.`;
}
body += `\n\n---\n*This is an automated response. A maintainer will review your issue soon.*`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: body
});
core.info('Posted response comment');
}
if (duplicateOf) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
state: 'closed',
state_reason: 'not_planned'
});
core.info(`Closed as duplicate of #${duplicateOf}`);
}
- name: 'Comment on failure'
if: failure()
uses: 'actions/github-script@v7'
env:
ISSUE_NUMBER: '${{ steps.get_issue.outputs.number || github.event.issue.number }}'
RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
with:
script: |
if (!process.env.ISSUE_NUMBER) return;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(process.env.ISSUE_NUMBER),
body: `⚠️ Automated triage failed. [View logs](${process.env.RUN_URL})`
});
================================================
FILE: .github/workflows/release-beta.yml
================================================
name: Release Beta
on:
workflow_dispatch:
inputs:
force:
description: 'Force publish even if version unchanged'
required: false
default: false
type: boolean
permissions:
contents: write
id-token: write
jobs:
publish-beta:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
always-auth: true
- name: Verify not on main branch
run: |
set -euo pipefail
CURRENT_BRANCH="${GITHUB_REF#refs/heads/}"
if [ "$CURRENT_BRANCH" = "main" ]; then
echo "ERROR: Beta release workflow should not run on main branch" >&2
echo "This workflow is for dev branch only" >&2
exit 1
fi
echo "Running on branch: $CURRENT_BRANCH"
- name: Determine and bump beta version
id: determine
run: |
set -euo pipefail
# Get base version from package.json (strip any existing prerelease suffix)
RAW_VERSION=$(node -p "require('./package.json').version")
BASE_VERSION=$(echo "$RAW_VERSION" | sed 's/-.*$//')
echo "base_version=$BASE_VERSION" >> "$GITHUB_OUTPUT"
# Fetch all tags
git fetch --tags --force
# Find the highest beta number for this base version
HIGHEST_BETA=-1
for tag in $(git tag -l "v${BASE_VERSION}-beta.*"); do
# Extract beta number from tag like v1.2.5-beta.3
BETA_NUM=$(echo "$tag" | sed "s/v${BASE_VERSION}-beta\\.//")
if [[ "$BETA_NUM" =~ ^[0-9]+$ ]] && [ "$BETA_NUM" -gt "$HIGHEST_BETA" ]; then
HIGHEST_BETA=$BETA_NUM
fi
done
# Also check npm for published beta versions
NPM_BETAS=$(npm view opencode-antigravity-auth versions --json 2>/dev/null | grep -oP "\"${BASE_VERSION}-beta\\.\\K[0-9]+" || echo "")
for BETA_NUM in $NPM_BETAS; do
if [ "$BETA_NUM" -gt "$HIGHEST_BETA" ]; then
HIGHEST_BETA=$BETA_NUM
fi
done
# Increment to next beta
NEXT_BETA=$((HIGHEST_BETA + 1))
NEXT_VERSION="${BASE_VERSION}-beta.${NEXT_BETA}"
echo "next_version=$NEXT_VERSION" >> "$GITHUB_OUTPUT"
echo "next_beta=$NEXT_BETA" >> "$GITHUB_OUTPUT"
echo "Base version: $BASE_VERSION"
echo "Highest existing beta: $HIGHEST_BETA"
echo "Next beta version: $NEXT_VERSION"
- name: Update package.json version
id: update_version
run: |
NEXT_VERSION="${{ steps.determine.outputs.next_version }}"
# Update package.json with new beta version
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.version = '$NEXT_VERSION';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 4) + '\n');
"
echo "Updated package.json to version $NEXT_VERSION"
# Commit the version bump
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json
git commit -m "chore: bump version to $NEXT_VERSION [skip ci]" || echo "No changes to commit"
git push origin dev || echo "Push failed or no changes"
- name: Check if should publish
id: should_publish
run: |
# Always publish since we auto-increment
echo "should_publish=true" >> "$GITHUB_OUTPUT"
echo "Will publish version ${{ steps.determine.outputs.next_version }}"
- name: Verify NPM token
if: steps.should_publish.outputs.should_publish == 'true'
run: |
if [ -z "${NPM_TOKEN}" ]; then
echo "NPM_TOKEN secret is required" >&2
exit 1
fi
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Install dependencies
if: steps.should_publish.outputs.should_publish == 'true'
run: npm install
- name: Run type check
if: steps.should_publish.outputs.should_publish == 'true'
run: npm run typecheck
- name: Run tests
if: steps.should_publish.outputs.should_publish == 'true'
run: npm test
- name: Build
if: steps.should_publish.outputs.should_publish == 'true'
run: npm run build
- name: Verify build artifacts
if: steps.should_publish.outputs.should_publish == 'true'
run: |
set -euo pipefail
[ -f dist/index.js ] || { echo "dist/index.js missing" >&2; exit 1; }
[ -f dist/index.d.ts ] || { echo "dist/index.d.ts missing" >&2; exit 1; }
[ -d dist/src ] || { echo "dist/src/ missing" >&2; exit 1; }
- name: Create git tag
if: steps.should_publish.outputs.should_publish == 'true'
run: |
NEXT_VERSION="${{ steps.determine.outputs.next_version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "v$NEXT_VERSION"
git push origin "v$NEXT_VERSION"
- name: Generate release notes
if: steps.should_publish.outputs.should_publish == 'true'
id: release_notes
run: |
set -euo pipefail
NEXT_VERSION="${{ steps.determine.outputs.next_version }}"
BASE_VERSION="${{ steps.determine.outputs.base_version }}"
# Get commits since last tag
LAST_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -n "$LAST_TAG" ]; then
CHANGELOG=$(git log --no-merges --pretty=format:'- %s (%h)' "${LAST_TAG}..HEAD" | grep -v "\[skip ci\]" || echo "")
COMPARE_URL="https://github.com/${GITHUB_REPOSITORY}/compare/${LAST_TAG}...v${NEXT_VERSION}"
else
CHANGELOG=$(git log --no-merges --pretty=format:'- %s (%h)' -20 | grep -v "\[skip ci\]" || echo "")
COMPARE_URL=""
fi
if [ -z "$CHANGELOG" ]; then
CHANGELOG="- Beta release ${NEXT_VERSION}"
fi
BODY_FILE=$(mktemp)
{
echo "## Beta Release v${NEXT_VERSION}"
echo ""
echo "⚠️ **This is a beta release for testing. Use at your own risk.**"
echo ""
echo "Base version: \`${BASE_VERSION}\`"
echo ""
if [ -n "$COMPARE_URL" ]; then
echo "Compare changes: $COMPARE_URL"
echo ""
fi
printf "%s\n" "$CHANGELOG"
echo ""
echo "### Install Beta"
echo ""
echo "Update your \`opencode.json\`:"
echo ""
printf '%s\n' '```json'
printf '%s\n' '{'
printf '%s\n' " \"plugin\": [\"opencode-antigravity-auth@${NEXT_VERSION}\"]"
printf '%s\n' '}'
printf '%s\n' '```'
echo ""
echo "Or use the beta tag (always latest beta):"
echo ""
printf '%s\n' '```json'
printf '%s\n' '{'
printf '%s\n' ' "plugin": ["opencode-antigravity-auth@beta"]'
printf '%s\n' '}'
printf '%s\n' '```'
echo ""
echo "Clear cache if stuck on old version:"
echo ""
printf '%s\n' '```bash'
printf '%s\n' 'rm -rf ~/.cache/opencode/node_modules ~/.cache/opencode/bun.lock'
printf '%s\n' '```'
} >"$BODY_FILE"
{
echo "body<<EOF"
cat "$BODY_FILE"
echo "EOF"
} >>"$GITHUB_OUTPUT"
- name: Create GitHub pre-release
if: steps.should_publish.outputs.should_publish == 'true'
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ steps.determine.outputs.next_version }}
release_name: v${{ steps.determine.outputs.next_version }} (Beta)
body: ${{ steps.release_notes.outputs.body }}
prerelease: true
draft: false
- name: Publish to npm with beta tag
if: steps.should_publish.outputs.should_publish == 'true'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --access public --tag beta --provenance
- name: Summary
if: steps.should_publish.outputs.should_publish == 'true'
run: |
echo "## Beta Release Published! 🚀" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ steps.determine.outputs.next_version }}" >> $GITHUB_STEP_SUMMARY
echo "**Base:** ${{ steps.determine.outputs.base_version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Install:**" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '{ "plugin": ["opencode-antigravity-auth@beta"] }' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
always-auth: true
- name: Determine release state
id: determine
run: |
set -euo pipefail
CURRENT_VERSION=$(node -p "require('./package.json').version")
echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
# Abort if version contains beta/alpha/rc (prerelease identifiers)
if echo "$CURRENT_VERSION" | grep -qE '(beta|alpha|rc)'; then
echo "ERROR: Cannot publish prerelease version '$CURRENT_VERSION' as official release" >&2
echo "This is main branch - version should not contain beta/alpha/rc" >&2
exit 1
fi
if git rev-parse HEAD^ >/dev/null 2>&1; then
PREVIOUS_VERSION=$(node -e "const { execSync } = require('node:child_process'); try { const data = execSync('git show HEAD^:package.json', { stdio: ['ignore', 'pipe', 'ignore'] }); const json = JSON.parse(data.toString()); if (json && typeof json.version === 'string') { process.stdout.write(json.version); } } catch (error) {}")
PREVIOUS_VERSION=${PREVIOUS_VERSION//$'\n'/}
else
PREVIOUS_VERSION=""
fi
echo "previous_version=$PREVIOUS_VERSION" >> "$GITHUB_OUTPUT"
if [ "$CURRENT_VERSION" = "$PREVIOUS_VERSION" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
git fetch --tags --force
if git tag -l "v$CURRENT_VERSION" | grep -q "v$CURRENT_VERSION"; then
echo "tag_exists=true" >> "$GITHUB_OUTPUT"
else
echo "tag_exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Verify NPM token
if: steps.determine.outputs.changed == 'true' && steps.determine.outputs.tag_exists == 'false'
run: |
if [ -z "${NPM_TOKEN}" ]; then
echo "NPM_TOKEN secret is required" >&2
exit 1
fi
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Install dependencies
if: steps.determine.outputs.changed == 'true' && steps.determine.outputs.tag_exists == 'false'
run: npm install
- name: Run Tests
if: steps.determine.outputs.changed == 'true' && steps.determine.outputs.tag_exists == 'false'
run: npm test
- name: Update README and Tag
if: steps.determine.outputs.changed == 'true' && steps.determine.outputs.tag_exists == 'false'
id: update_readme
run: |
CURRENT_VERSION="${{ steps.determine.outputs.current_version }}"
git checkout main
git pull origin main
# Update version in README.md (looking for pattern opencode-antigravity-auth@x.y.z)
sed -i "s/opencode-antigravity-auth@[0-9]\+\.[0-9]\+\.[0-9]\+/opencode-antigravity-auth@$CURRENT_VERSION/g" README.md
if ! git diff --quiet README.md; then
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add README.md
git commit -m "docs: update readme version to $CURRENT_VERSION [skip ci]"
git push origin main
echo "README updated"
else
echo "README already up to date"
fi
# Create and push tag on the current HEAD
git tag "v$CURRENT_VERSION"
git push origin "v$CURRENT_VERSION"
- name: Build
if: steps.determine.outputs.changed == 'true' && steps.determine.outputs.tag_exists == 'false'
run: npm run build
- name: Verify build artifacts
if: steps.determine.outputs.changed == 'true' && steps.determine.outputs.tag_exists == 'false'
run: |
set -euo pipefail
[ -f dist/index.js ] || { echo "dist/index.js missing" >&2; exit 1; }
[ -f dist/index.d.ts ] || { echo "dist/index.d.ts missing" >&2; exit 1; }
[ -d dist/src ] || { echo "dist/src/ missing" >&2; exit 1; }
- name: Generate release notes
if: steps.determine.outputs.changed == 'true' && steps.determine.outputs.tag_exists == 'false'
id: release_notes
run: |
set -euo pipefail
CURRENT_VERSION="${{ steps.determine.outputs.current_version }}"
PREVIOUS_VERSION="${{ steps.determine.outputs.previous_version }}"
RANGE=""
COMPARE_URL=""
LAST_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || true)
if [ -z "$LAST_TAG" ] && [ -n "$PREVIOUS_VERSION" ] && git rev-parse "refs/tags/v${PREVIOUS_VERSION}" >/dev/null 2>&1; then
LAST_TAG="v${PREVIOUS_VERSION}"
fi
if [ -n "$LAST_TAG" ]; then
RANGE="${LAST_TAG}..HEAD"
COMPARE_URL="https://github.com/${GITHUB_REPOSITORY}/compare/${LAST_TAG}...v${CURRENT_VERSION}"
fi
if [ -n "$RANGE" ]; then
CHANGELOG=$(git log --no-merges --pretty=format:'- %s (%h)' "$RANGE")
else
CHANGELOG=$(git log --no-merges --pretty=format:'- %s (%h)')
fi
if [ -z "$CHANGELOG" ]; then
CHANGELOG="- No commits found for this release."
fi
BODY_FILE=$(mktemp)
{
echo "## Release v${CURRENT_VERSION}"
echo ""
if [ -n "$COMPARE_URL" ]; then
echo "Compare changes: $COMPARE_URL"
echo ""
fi
printf "%s\n" "$CHANGELOG"
echo ""
echo "### Upgrade"
echo ""
echo "Update your \`opencode.json\`:"
echo ""
printf '%s\n' '```json'
printf '%s\n' '{'
printf '%s\n' " \"plugins\": [\"opencode-antigravity-auth@${CURRENT_VERSION}\"]"
printf '%s\n' '}'
printf '%s\n' '```'
echo ""
echo "If stuck on an old version, clear the cache:"
echo ""
printf '%s\n' '```bash'
printf '%s\n' 'rm -rf ~/.cache/opencode/node_modules ~/.cache/opencode/bun.lock'
printf '%s\n' '```'
} >"$BODY_FILE"
cat "$BODY_FILE"
{
echo "body<<EOF"
cat "$BODY_FILE"
echo "EOF"
} >>"$GITHUB_OUTPUT"
- name: Create GitHub release
if: steps.determine.outputs.changed == 'true' && steps.determine.outputs.tag_exists == 'false'
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ steps.determine.outputs.current_version }}
release_name: v${{ steps.determine.outputs.current_version }}
body: ${{ steps.release_notes.outputs.body }}
generate_release_notes: false
- name: Publish to npm
if: steps.determine.outputs.changed == 'true' && steps.determine.outputs.tag_exists == 'false'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --access public --provenance
================================================
FILE: .github/workflows/republish-version.yml
================================================
name: Republish Version
on:
workflow_dispatch:
inputs:
version:
description: 'Version to republish (e.g., 1.2.5)'
required: true
type: string
skip_unpublish:
description: 'Skip unpublish step (if version does not exist on npm)'
required: false
default: false
type: boolean
permissions:
contents: write
id-token: write
jobs:
republish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
always-auth: true
- name: Verify version matches package.json
run: |
set -euo pipefail
REQUESTED_VERSION="${{ github.event.inputs.version }}"
PACKAGE_VERSION=$(node -p "require('./package.json').version")
if [ "$REQUESTED_VERSION" != "$PACKAGE_VERSION" ]; then
echo "ERROR: Requested version ($REQUESTED_VERSION) does not match package.json ($PACKAGE_VERSION)" >&2
echo "Please update package.json to the version you want to publish" >&2
exit 1
fi
echo "Version verified: $PACKAGE_VERSION"
- name: Unpublish existing version
if: github.event.inputs.skip_unpublish != 'true'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
VERSION="${{ github.event.inputs.version }}"
echo "Checking if version $VERSION exists on npm..."
if npm view opencode-antigravity-auth@$VERSION version >/dev/null 2>&1; then
echo "Version $VERSION exists, unpublishing..."
npm unpublish opencode-antigravity-auth@$VERSION --force
echo "Successfully unpublished version $VERSION"
else
echo "Version $VERSION does not exist on npm, skipping unpublish"
fi
- name: Delete existing git tag
run: |
set -euo pipefail
VERSION="${{ github.event.inputs.version }}"
TAG="v$VERSION"
echo "Checking if tag $TAG exists..."
if git tag -l "$TAG" | grep -q "$TAG"; then
echo "Tag $TAG exists locally, deleting..."
git tag -d "$TAG" || true
fi
if git ls-remote --tags origin | grep -q "refs/tags/$TAG"; then
echo "Tag $TAG exists remotely, deleting..."
git push origin :refs/tags/$TAG || true
fi
echo "Git tag cleanup complete"
- name: Verify NPM token
run: |
if [ -z "${NPM_TOKEN}" ]; then
echo "NPM_TOKEN secret is required" >&2
exit 1
fi
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Install dependencies
run: npm install
- name: Run type check
run: npm run typecheck
- name: Run tests
run: npm test
- name: Build
run: npm run build
- name: Verify build artifacts
run: |
set -euo pipefail
[ -f dist/index.js ] || { echo "dist/index.js missing" >&2; exit 1; }
[ -f dist/index.d.ts ] || { echo "dist/index.d.ts missing" >&2; exit 1; }
[ -d dist/src ] || { echo "dist/src/ missing" >&2; exit 1; }
- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
VERSION="${{ github.event.inputs.version }}"
echo "Publishing version $VERSION to npm..."
npm publish --access public --provenance
echo "Successfully published version $VERSION"
echo ""
echo "Updating @latest tag..."
npm dist-tag add opencode-antigravity-auth@$VERSION latest
echo ""
echo "Current dist-tags:"
npm dist-tag ls opencode-antigravity-auth
- name: Create git tag
run: |
VERSION="${{ github.event.inputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "v$VERSION"
git push origin "v$VERSION"
- name: Generate release notes
id: release_notes
run: |
set -euo pipefail
VERSION="${{ github.event.inputs.version }}"
LAST_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -n "$LAST_TAG" ]; then
CHANGELOG=$(git log --no-merges --pretty=format:'- %s (%h)' "${LAST_TAG}..HEAD" | grep -v "\[skip ci\]" || echo "")
COMPARE_URL="https://github.com/${GITHUB_REPOSITORY}/compare/${LAST_TAG}...v${VERSION}"
else
CHANGELOG=$(git log --no-merges --pretty=format:'- %s (%h)' -20 | grep -v "\[skip ci\]" || echo "")
COMPARE_URL=""
fi
if [ -z "$CHANGELOG" ]; then
CHANGELOG="- Release v${VERSION}"
fi
BODY_FILE=$(mktemp)
{
echo "## Release v${VERSION}"
echo ""
if [ -n "$COMPARE_URL" ]; then
echo "Compare changes: $COMPARE_URL"
echo ""
fi
printf "%s\n" "$CHANGELOG"
echo ""
echo "### Install"
echo ""
echo "Update your \`opencode.json\`:"
echo ""
printf '%s\n' '```json'
printf '%s\n' '{'
printf '%s\n' " \"plugins\": [\"opencode-antigravity-auth@${VERSION}\"]"
printf '%s\n' '}'
printf '%s\n' '```'
} >"$BODY_FILE"
{
echo "body<<EOF"
cat "$BODY_FILE"
echo "EOF"
} >>"$GITHUB_OUTPUT"
- name: Create GitHub release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ github.event.inputs.version }}
release_name: v${{ github.event.inputs.version }}
body: ${{ steps.release_notes.outputs.body }}
draft: false
prerelease: false
- name: Summary
run: |
echo "## Version Republished! 🚀" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ github.event.inputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Install:**" >> $GITHUB_STEP_SUMMARY
echo '```bash' >> $GITHUB_STEP_SUMMARY
echo "npm install opencode-antigravity-auth@${{ github.event.inputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "# or" >> $GITHUB_STEP_SUMMARY
echo "npm install opencode-antigravity-auth@latest" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
================================================
FILE: .github/workflows/update-dist-tag.yml
================================================
name: Update NPM Dist Tag
on:
workflow_dispatch:
inputs:
version:
description: 'Version to tag (e.g., 1.2.5)'
required: true
type: string
tag:
description: 'Dist tag to update (e.g., latest, beta)'
required: true
type: choice
options:
- latest
- beta
- next
- canary
permissions:
contents: read
id-token: write
jobs:
update-tag:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
always-auth: true
- name: Validate version exists
run: |
set -euo pipefail
VERSION="${{ github.event.inputs.version }}"
TAG="${{ github.event.inputs.tag }}"
echo "Checking if version $VERSION exists on npm..."
if ! npm view opencode-antigravity-auth@$VERSION version >/dev/null 2>&1; then
echo "ERROR: Version $VERSION does not exist on npm" >&2
echo "Available versions:" >&2
npm view opencode-antigravity-auth versions --json | tail -20 >&2
exit 1
fi
echo "Version $VERSION exists on npm"
echo "Current dist-tags:"
npm dist-tag ls opencode-antigravity-auth
- name: Update dist-tag
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
VERSION="${{ github.event.inputs.version }}"
TAG="${{ github.event.inputs.tag }}"
echo "Updating @$TAG tag to version $VERSION..."
npm dist-tag add opencode-antigravity-auth@$VERSION $TAG
echo ""
echo "Updated dist-tags:"
npm dist-tag ls opencode-antigravity-auth
- name: Summary
run: |
echo "## NPM Dist Tag Updated! 🏷️" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ github.event.inputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "**Tag:** @${{ github.event.inputs.tag }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Users can now install with:" >> $GITHUB_STEP_SUMMARY
echo '```bash' >> $GITHUB_STEP_SUMMARY
echo "npm install opencode-antigravity-auth@${{ github.event.inputs.tag }}" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
================================================
FILE: .gitignore
================================================
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
antigravity-debug-*.log
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
# Hive workflow internal files
.hive/
# Test artifacts
test-file.ts
# Local subrepos (not part of this project)
CLIProxyAPI/
LLM-API-Key-Proxy/
opencode/
opencode-better-antigravity-auth/
================================================
FILE: AGENTS.MD
================================================
# AGENTS.md
Guidance for AI agents working with this repository.
## Overview
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.
## Build & Test Commands
```bash
npm install # Install dependencies
npm run build # Compile (tsc -p tsconfig.build.json)
npm run typecheck # Type-check only (tsc --noEmit)
npm test # Run all tests (vitest run)
npx vitest run src/plugin/auth.test.ts # Single test file
npx vitest run -t "test name here" # Single test by name
npx vitest --watch src/plugin/auth.test.ts # Watch mode, single file
npm run test:coverage # Coverage report
npm run test:e2e:models # E2E: model availability check
npm run test:e2e:regression # E2E: regression suite
```
No linter or formatter is configured. Style is enforced by convention (see below).
## TypeScript Configuration
- `strict: true` with extra strictness: `noUncheckedIndexedAccess`, `noImplicitOverride`, `noFallthroughCasesInSwitch`
- `verbatimModuleSyntax: true` — use `import type` for type-only imports
- `target: ESNext`, `module: Preserve`, `moduleResolution: bundler`
- `allowImportingTsExtensions: true` — use `.ts` extensions in imports
- No path aliases — all imports are relative
## Code Style
### Imports
- Use `import type { ... }` for type-only imports (enforced by `verbatimModuleSyntax`)
- Named imports only — no default imports in src/
- Relative paths with `.ts` extensions: `import { foo } from "./bar.ts"`
- Order: node builtins > external packages > local modules
### Exports
- Named exports only in src/ — no default exports
- Barrel files (index.ts) for module surfaces
### Naming
- `camelCase` for functions, variables, parameters
- `PascalCase` for types, interfaces, classes, enums
- `UPPER_SNAKE_CASE` for constants
- `kebab-case` for file names (e.g., `request-helpers.ts`, `thinking-recovery.ts`)
- Test files: `*.test.ts` colocated with source
### Types
- No `I` prefix on interfaces, no `Type` suffix
- Use `z.infer<typeof Schema>` for Zod-derived types
- Extract to `types.ts` when shared, inline when local
- Discriminated unions preferred over boolean flags
- Never use `as any`, `@ts-ignore`, or `@ts-expect-error`
### Functions
- `export function` for public APIs
- Arrow functions for callbacks, factories, and inline closures
- Async functions with targeted try/catch (not blanket)
### Error Handling
- 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
### Formatting
- 2-space indentation
- Double quotes for strings
- Trailing commas in multiline constructs
- No semicolons (project convention)
### Logging
- `createLogger("module-name")` for structured logging
- `console.log` only for CLI/user-facing output
## Module Structure
```
src/
├── plugin.ts # Main entry, fetch interceptor
├── constants.ts # Endpoints, headers, API config, system prompts
├── antigravity/oauth.ts # OAuth token exchange
└── plugin/
├── auth.ts # Token validation & refresh
├── request.ts # Request transformation (core logic)
├── request-helpers.ts # Schema cleaning, thinking filters
├── thinking-recovery.ts # Turn boundary detection
├── recovery.ts # Session recovery (tool_result_missing)
├── quota.ts # Quota checking (API usage stats)
├── cache.ts # Auth & signature caching
├── accounts.ts # Multi-account management & storage
├── storage.ts # Persistent storage schemas (Zod)
├── fingerprint.ts # Device fingerprint generation & headers
├── project.ts # Managed project context resolution
└── debug.ts # Debug logging utilities
```
## Key Design Patterns
### 1. Request Interception
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).
### 2. Claude Thinking Blocks
ALL thinking blocks are stripped from outgoing requests for Claude models. Claude generates fresh thinking each turn. This eliminates signature validation errors.
### 3. Session Recovery
When tool execution is interrupted (ESC/timeout), the plugin injects synthetic `tool_result` blocks to recover the session without starting over.
### 4. Schema Sanitization
Tool schemas are cleaned via allowlist. Unsupported fields (`const`, `$ref`, `$defs`) are removed or converted to Antigravity-compatible format.
### 5. Multi-Account Load Balancing
Accounts rotate on rate limits. Gemini has dual quota pools (Antigravity headers + Gemini CLI headers). Fingerprints are per-account and regenerated on capacity exhaustion.
### 6. Fingerprint System
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.
## Dependencies
- `zod ^4` — schema validation (NOT zod v3)
- `@opencode-ai/plugin` — OpenCode plugin interface
- `@openauthjs/openauth` — OAuth client
- `proper-lockfile` — file locking for concurrent access
- `xdg-basedir` — XDG directory resolution
## Testing
- Framework: **Vitest 3** with native ESM
- Config: `vitest.config.ts`
- Tests colocated: `src/plugin/foo.test.ts` next to `src/plugin/foo.ts`
- Use `describe`/`it`/`expect` — standard Vitest API
- Mock with `vi.fn()`, `vi.spyOn()`, `vi.mock()`
## Documentation
- [README.md](README.md) — Installation & usage
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — Detailed architecture guide
- [docs/ANTIGRAVITY_API_SPEC.md](docs/ANTIGRAVITY_API_SPEC.md) — API reference
- [CHANGELOG.md](CHANGELOG.md) — Version history
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## [1.6.0] - 2026-02-20
### Fixed
- **#397** - Gemini tool-call payload handling now enforces valid `thought_signature` behavior for `functionCall` parts, preventing `400 INVALID_ARGUMENT` in mixed and parallel call turns.
- **#454** - Request sanitization now removes empty/invalid `contents.parts` entries and invalid `systemInstruction.parts` before forwarding to Antigravity.
- **#444** - Response transform fallback now uses cloned responses and preserves recovery signaling, eliminating `Body already used` failures.
- **#368 (Tackled)** - Claude thinking/signature handling now replaces foreign signatures with sentinels and tightens thinking-order classification to reduce false-positive recovery triggers.
### Changed
- **Debug Sink Split** - `debug` now controls file logging only, while `debug_tui` independently controls TUI panel logging.
- **Header Normalization** - `x-goog-user-project` is now stripped across Antigravity and Gemini CLI request styles.
- **Claude Prompt Auto-Caching (Optional)** - Added `claude_prompt_auto_caching` to inject `cache_control: { type: "ephemeral" }` when Claude prompt caching is desired and unset.
### Documentation
- Updated README, architecture/config/troubleshooting docs, and generated schema docs to reflect new debug sink semantics and config keys.
## [1.5.2] - 2026-02-18
### Changed
- Added support for Sonnet 4.6 and removed old models support.
## [1.5.1] - 2026-02-11
### Changed
- **Header Identity Alignment** - `ideType` changed from `IDE_UNSPECIFIED` to `ANTIGRAVITY` and `platform` from `PLATFORM_UNSPECIFIED` to dynamic `WINDOWS`/`MACOS` (based on `process.platform`) across all header sources (`getAntigravityHeaders`, `oauth.ts`, `project.ts`). Now matches Antigravity Manager behavior
- **Gemini CLI `Client-Metadata` Header** - Gemini CLI requests now include `Client-Metadata` header, aligning with actual `gemini-cli` behavior. Previously only Antigravity-style requests sent this header
- **Gemini CLI User-Agent Format** - Updated from `GeminiCLI/{ver}/{model}` to `GeminiCLI/{ver}/{model} ({platform}; {arch})` to match real `gemini-cli` UA strings. Version pool updated from `1.2.0/1.1.0/1.0.0` to `0.28.0/0.27.4/0.27.3` to align with actual release numbers
- **Randomized Headers Model-Aware** - `getRandomizedHeaders()` now accepts an optional `model` parameter, embedding the actual model name in Gemini CLI User-Agent strings instead of a hardcoded default
- **Fingerprint Platform Alignment** - Antigravity-style `Client-Metadata` platform now consistently matches the randomized User-Agent platform, fixing a potential mismatch where headers could disagree on reported platform
### Removed
- **Linux Fingerprints** - Removed `linux/amd64` and `linux/arm64` from `ANTIGRAVITY_PLATFORMS` and fingerprint generation. Linux users now masquerade as macOS (Antigravity does not support Linux as a native platform)
- **`getAntigravityUserAgents()` Function** - Removed unused helper that had no callers in the codebase
- **`X-Opencode-Tools-Debug` Header** - Removed debug telemetry header from outgoing requests
## [1.5.0] - 2026-02-11
### Added
- **Account Verification Flow** - Auth login menu now supports `verify` and `verify-all` actions. When Antigravity returns a 403 with `validation_required`, the account is automatically disabled, marked with a verification URL, and cooled down. Users can verify accounts directly from the menu with a probe request to confirm resolution
- **Dynamic Antigravity Version** - Plugin version is now fetched at startup from the Antigravity updater API, with a changelog-scrape fallback and a hardcoded last-resort. Eliminates stale "version no longer supported" errors after Antigravity updates
- **Storage V4 Schema** - New storage version adds `verificationRequired`, `verificationRequiredAt`, `verificationRequiredReason`, `verificationUrl`, and `fingerprintHistory` fields per account. Full migration chain from v1/v2/v3 to v4
- **`saveAccountsReplace`** - New destructive-write storage function that replaces the entire accounts file without merging, preventing deleted accounts from being resurrected by concurrent reads
- **`setAccountEnabled` / Account Toggling** - New account management methods: `setAccountEnabled()`, `markAccountVerificationRequired()`, `clearAccountVerificationRequired()`, `removeAccountByIndex()`
- **Secure File Permissions** - Credential storage files are now created with mode `0600` (owner read/write only). Existing files with overly permissive modes are tightened on load
- **`opencode.jsonc` Support** - Configure models flow now detects and prefers existing `opencode.jsonc` files. JSONC parsing strips comments and trailing commas before JSON.parse
- **Header Contract Tests** - New `src/constants.test.ts` validates header shapes, randomization behavior, and optional header fields for both Antigravity and Gemini CLI styles
### Changed
- **Unified Gemini Routing** - Gemini quota fallback between Antigravity and Gemini CLI pools is now always enabled for Gemini models. The `quota_fallback` config flag is deprecated and ignored (backward-compatible, no breakage)
- **`cli_first` Honored in Routing** - `resolveHeaderRoutingDecision()` centralizes routing logic and properly respects `cli_first` for unsuffixed Gemini models
- **Fingerprint Headers Simplified** - `buildFingerprintHeaders()` now returns only `User-Agent`. Removed `X-Goog-QuotaUser`, `X-Client-Device-Id`, `X-Goog-Api-Client`, and `Client-Metadata` from outgoing content requests to align with Antigravity Manager behavior
- **Client Metadata Reduced** - Fingerprint client metadata trimmed to `ideType`, `platform`, `pluginType` only. Removed `osVersion`, `arch`, `sqmId`
- **Gemini CLI User-Agent Format** - Updated from `google-genai-sdk/...` to `GeminiCLI/...` format
- **Search Model** - Changed from `gemini-2.0-flash` to `gemini-2.5-flash` for improved search result quality
- **Deterministic Search Generation** - Search requests now use `temperature: 0` and `topP: 1` instead of thinking config
- **OAuth Headers Dynamic** - `oauth.ts` and `project.ts` now use `getAntigravityHeaders()` instead of static constants, removing stale `X-Goog-Api-Client` from token/project calls
### Fixed
- **#410**: Strip `x-goog-user-project` header for ALL header styles, not just Antigravity. This header caused 403 errors on Daily/Prod endpoints when the user's GCP project lacked Cloud Code API
- **#370 / #336**: Account deletion now persists correctly. Root cause: `saveAccounts()` merged deleted accounts back from disk. Fixed by introducing `saveAccountsReplace()` for destructive writes and syncing in-memory state immediately
- **#381**: Disabled accounts no longer selected via sticky index. `getCurrentAccountForFamily()` now skips disabled accounts and advances the active index
- **#384**: `google_search` tool no longer returns empty citations when using `gemini-3-flash`. Search model switched to `gemini-2.5-flash`
- **#377**: Configure models flow now respects existing `opencode.jsonc` files instead of creating duplicate `opencode.json`
- **Excessive Disk Writes** - Fixed project context auth updates causing 3000+ writes/sec during streaming. Changed from reference equality to value comparison on auth tokens and added throttled saves. Prevents SSD wear on macOS
- **Fingerprint Alignment** - Force-regenerated fingerprints to match current Antigravity Manager behavior, fixing `ideType` and stripping stale client metadata fields
### Removed
- **Extra Outgoing Headers** - `X-Goog-Api-Client`, `Client-Metadata`, `X-Goog-QuotaUser`, `X-Client-Device-Id` no longer sent on content requests
- **Fingerprint Metadata Fields** - `osVersion`, `arch`, `sqmId` removed from fingerprint client metadata
- **`updateFingerprintVersion` Helper** - Removed from accounts module (fingerprint version rewriting no longer needed)
### Documentation
- **AGENTS.md** expanded with detailed architecture, code style, and fingerprint system documentation
- **README.md**, **CONFIGURATION.md**, **MULTI-ACCOUNT.md** updated to reflect deprecated `quota_fallback` and automatic Gemini pool fallback behavior
- **`antigravity.schema.json`** marks `quota_fallback` as deprecated/ignored
## [1.4.5] - 2026-02-05
### Added
- **Configure Models Menu Action** - Auth login menu now includes a "Configure models" action that writes plugin model definitions directly into `opencode.json`, making setup easier for new users
- **`cli_first` Config Option** - New configuration option to route Gemini models to Gemini CLI quota first, useful for users who want to preserve Antigravity quota for Claude models
- **`toast_scope` Configuration** - Control toast visibility per session with `toast_scope: "root_only"` to suppress toasts in subagent sessions
- **Soft Quota Protection** - Skip accounts over 90% usage threshold to prevent Google penalties, with configurable `soft_quota_threshold_percent` and wait/retry behavior
- **Gemini CLI Quota Management** - Enhanced quota display with dual quota pool support (Antigravity + Gemini CLI)
- **`OPENCODE_CONFIG_DIR` Environment Variable** - Custom config location support for non-standard setups
- **`quota_refresh_interval_minutes`** - Background quota cache refresh (default 15 minutes)
- **`soft_quota_cache_ttl_minutes`** - Cache freshness control for soft quota checks
### Changed
- **Model Naming and Routing** - Documented antigravity-prefixed model names and automatic mapping to CLI preview names (e.g., `antigravity-gemini-3-flash` → `gemini-3-flash-preview`)
- **Antigravity-First Quota Strategy** - Exhausts Antigravity quota across ALL accounts before falling back to Gemini CLI quota (previously per-account)
- **Quota Routing Respects `cli_first`** - Fallback behavior updated to respect `cli_first` preference
- **Config Directory Resolution** - Now prioritizes `OPENCODE_CONFIG_DIR` environment variable
- **Enhanced Debug Logging** - Process ID included for better traceability across concurrent sessions
- **Improved Quota Group Resolution** - More consistent quota management with `resolveQuotaGroup` function
### Fixed
- **#337**: Skip disabled accounts in proactive token refresh
- **#233**: Skip sandbox endpoints for Gemini CLI models (fixes 404/403 cascade)
- **Windows Config Auto-Migration**: Automatically migrates config from `%APPDATA%\opencode\` to `~/.config/opencode/`
- **Root Session Detection**: Reset `isChildSession` flag correctly for root sessions
- **Stale Quota Cache**: Prevent spin loop on stale quota cache
- **Quota Group Default**: Fix quota group selection defaulting to `gemini-pro` when model is null
### Removed
- **Fingerprint Headers for Gemini CLI** - Removed fingerprint headers from Gemini CLI model requests to align with official behavior
- **`web_search` Configuration Leftovers** - Cleaned up remaining `web_search` config remnants from schema
### Documentation
- Updated README with model configuration options and simplified setup instructions
- Updated MODEL-VARIANTS.md with Antigravity model names and configuration guidance
- Updated CONFIGURATION.md to clarify `quota_fallback` behavior across accounts
- Updated MULTI-ACCOUNT.md with dual quota pool and fallback flow details
---
## [1.3.2] - 2026-01-27
### Added
- **Quota check and account management in auth login** - Added new `--quota` and `--manage` options to the `auth login` command for checking account quota status and managing accounts directly from the CLI ([#284](https://github.com/NoeFabris/opencode-antigravity-auth/issues/284))
- **Request timing jitter** - Added configurable random delay to requests to reduce detection patterns and improve rate limit resilience. Requests now include small random timing variations
- **Header randomization for fingerprint diversity** - Headers are now randomized to create more diverse fingerprints, reducing the likelihood of requests being grouped and rate-limited together
- **Per-account fingerprint persistence** - Fingerprints are now persisted per-account in storage, allowing consistent identity across sessions and enabling fingerprint history tracking
- Added fingerprint restore operations to AccountManager
- Extended per-account fingerprint history for better tracking
- Fingerprint now shown in debug output
- **Scheduling mode configuration** - Added new scheduling modes including `cache-first` mode that prioritizes accounts with cached tokens, reducing authentication overhead
- **Failure count TTL expiration** - Account failure counts now expire after a configurable time period, allowing accounts to naturally recover from temporary issues
- **Exponential backoff for 503/529 errors** - Implemented exponential backoff with jitter for capacity-related errors, matching behavior of Antigravity-Manager
### Changed
- **Increased MODEL_CAPACITY backoff to 45s with jitter** - Extended the base backoff time for model capacity errors from previous values to 45 seconds, with added jitter to prevent thundering herd issues
- **Regenerate fingerprint after capacity retry exhaustion** - When all capacity retries are exhausted, the fingerprint is now regenerated to potentially get assigned to a different backend partition
- **Enhanced duration parsing for Go format** - Improved parsing of duration strings to handle Go-style duration formats (e.g., `1h30m`) used in some API responses
### Fixed
- **Prevent toast spam for rate limit warnings** - Added 5-second debounce for rate limit warning toasts to prevent notification flooding when multiple requests hit rate limits simultaneously ([#286](https://github.com/NoeFabris/opencode-antigravity-auth/issues/286))
- **`getEnabledAccounts` now treats undefined as enabled** - Fixed issue where accounts without an explicit `enabled` field were incorrectly filtered out. Accounts now default to enabled when the field is undefined
- **Show correct position in account toast for enabled accounts** - Fixed the account position indicator in toast notifications to only count enabled accounts, showing accurate position like "Account 2/5" instead of including disabled accounts
- **Filter disabled accounts in all selection methods** - Ensured disabled accounts are properly excluded from all account selection strategies (round-robin, least-used, random, etc.)
- **Robust handling for capacity/5xx errors** - Implemented comprehensive retry logic for model capacity and server errors, achieving parity with Antigravity-Manager's behavior
- Reordered parsing logic to prioritize capacity checks
- Fixed loop retry logic to prevent state pollution
- Added capacity retry limit to prevent infinite loops ([#263](https://github.com/NoeFabris/opencode-antigravity-auth/issues/263))
- **Fixed @opencode-ai/plugin dependency location** - Moved `@opencode-ai/plugin` from devDependencies to dependencies section, fixing runtime errors when the plugin was installed without dev dependencies
### Removed
- **Removed deprecated `web_search` configuration** - The deprecated `web_search.default_mode` and `web_search.grounding_threshold` configuration options have been fully removed. Use the `google_search` tool instead (introduced in 1.3.1)
## [1.3.1] - 2026-01-21
### Added
- **New `google_search` tool for web search** - Implements Google Search grounding as a callable tool that the model can invoke explicitly
- Makes separate API calls with only `{ googleSearch: {} }` tool, avoiding Gemini API limitation where grounding tools cannot be combined with function declarations
- Returns formatted markdown with search results, sources with URLs, and search queries used
- Supports optional URL analysis via `urlContext` when URLs are provided
- Configurable thinking mode (deep vs fast) for search operations
- Uses `gemini-3-flash` model for fast, cost-effective search operations
### Changed
- Upgraded to Zod v4 and adjusted schema generation for compatibility
- **Deprecated `web_search` config** - The `web_search.default_mode` and `web_search.grounding_threshold` config options are now deprecated. Google Search is now implemented as a dedicated tool rather than automatic grounding injection
### Fixed
- **`keep_thinking=true` now works without debug mode** - Fixed Claude multi-turn conversations failing with "Failed to process error response" when `keep_thinking=true` after tool calls, unless debug mode was enabled
- Root cause: `filterContentArray` trusted any signature >= 50 chars for last assistant messages, but Claude returns its own signatures that Antigravity doesn't recognize
- Fix: Now verifies signatures against our cache via `isOurCachedSignature()` before passing through. Foreign/missing signatures get replaced with `SKIP_THOUGHT_SIGNATURE` sentinel
- Why debug worked: Debug mode injects synthetic thinking with no signature, triggering sentinel injection correctly
- **Fixed tool calls failing for tools with no parameters** - Tools like `hive_plan_read`, `hive_status`, and `hive_feature_list` that have no required parameters would fail with Zod validation error `state.input: expected record, received undefined`
- Root cause: When Claude calls a tool with no parameters, it returns `functionCall` without an `args` field. The response transformation only processed parts where `functionCall.args` was defined, leaving `args` as `undefined`
- Fix: Changed condition to handle all `functionCall` parts, defaulting `args` to `{}` when missing, ensuring opencode's `state.input` always receives a valid record
- **Auth headers aligned with official Gemini CLI** - Updated authentication headers to match the official Antigravity/Gemini CLI behavior, reducing "account ineligible" errors and potential bans ([#178](https://github.com/NoeFabris/opencode-antigravity-auth/issues/178))
- `GEMINI_CLI_HEADERS["User-Agent"]`: `9.15.1` → `10.3.0`
- `GEMINI_CLI_HEADERS["X-Goog-Api-Client"]`: `gl-node/22.17.0` → `gl-node/22.18.0`
- `ANTIGRAVITY_HEADERS["User-Agent"]`: Updated to full Chrome/Electron user agent string
- Token exchange now includes `Accept`, `Accept-Encoding`, `User-Agent`, `X-Goog-Api-Client` headers
- Userinfo fetch now includes `User-Agent`, `X-Goog-Api-Client` headers
- `fetchProjectID` now uses centralized constants instead of hardcoded strings
- **`quiet_mode` now properly suppresses all toast notifications** - Fixed `quiet_mode: true` in `antigravity.json` not suppressing "Status dialog dismissed" and other toast notifications ([#207](https://github.com/NoeFabris/opencode-antigravity-auth/issues/207))
- Root cause: The `showToast` helper function didn't check `quietMode`, and only some call sites had manual `!quietMode &&` guards
- Fix: Moved `quietMode` check inside `showToast` helper so all toasts are automatically suppressed when `quiet_mode: true`
### Removed
- **Removed automatic `googleSearch` injection** - Previously attempted to inject `{ googleSearch: {} }` into all Gemini requests, which never worked due to API limitations. Now uses the explicit tool approach instead
## [1.3.0] - Previous Release
See [releases](https://github.com/NoeFabris/opencode-antigravity-auth/releases) for previous versions.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 Jens
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Antigravity + Gemini CLI OAuth Plugin for Opencode
[](https://www.npmjs.com/package/opencode-antigravity-auth)
[](https://www.npmjs.com/package/opencode-antigravity-auth)
[](https://www.npmjs.com/package/opencode-antigravity-auth)
[](LICENSE)
[](https://x.com/dopesalmon)
Enable Opencode to authenticate against **Antigravity** (Google's IDE) via OAuth so you can use Antigravity rate limits and access models like `gemini-3.1-pro` and `claude-opus-4-6-thinking` with your Google credentials.
## What You Get
- **Claude Opus 4.6, Sonnet 4.6** and **Gemini 3.1 Pro/Flash** via Google OAuth
- **Multi-account support** — add multiple Google accounts, auto-rotates when rate-limited
- **Dual quota system** — access both Antigravity and Gemini CLI quotas from one plugin
- **Thinking models** — extended thinking for Claude and Gemini 3 with configurable budgets
- **Google Search grounding** — enable web search for Gemini models (auto or always-on)
- **Auto-recovery** — handles session errors and tool failures automatically
- **Plugin compatible** — works alongside other OpenCode plugins (oh-my-opencode, dcp, etc.)
---
<details open>
<summary><b>⚠️ Terms of Service Warning — Read Before Installing</b></summary>
> [!CAUTION]
> Using this plugin (and any proxy for antgravity) violate Google's Terms of Service. A number of users have reported their Google accounts being **banned** or **shadow-banned** (restricted access without explicit notification).
>
> **By using this plugin, you acknowledge:**
> - This is an unofficial tool not endorsed by Google
> - Your account may be suspended or permanently banned
> - You assume all risks associated with using this plugin
>
</details>
---
## Installation
<details open>
<summary><b>For Humans</b></summary>
**Option A: Let an LLM do it**
Paste this into any LLM agent (Claude Code, OpenCode, Cursor, etc.):
```
Install the opencode-antigravity-auth plugin and add the Antigravity model definitions to ~/.config/opencode/opencode.json by following: https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/dev/README.md
```
**Option B: Manual setup**
1. **Add the plugin** to `~/.config/opencode/opencode.json`:
```json
{
"plugin": ["opencode-antigravity-auth@latest"]
}
```
> Want bleeding-edge features? Use `opencode-antigravity-auth@beta` instead.
2. **Login** with your Google account:
```bash
opencode auth login
```
3. **Add models** — choose one:
- Run `opencode auth login` → Google → OAuth with Google (Antigravity) → select **"Configure models in opencode.json"** (auto-configures all models)
- Or manually copy the [full configuration](#models) below
4. **Use it:**
```bash
opencode run "Hello" --model=google/antigravity-claude-opus-4-6-thinking --variant=max
```
</details>
<details>
<summary><b>For LLM Agents</b></summary>
### Step-by-Step Instructions
1. Edit the OpenCode configuration file at `~/.config/opencode/opencode.json`
> **Note**: This path works on all platforms. On Windows, `~` resolves to your user home directory (e.g., `C:\Users\YourName`).
2. Add the plugin to the `plugin` array
3. Add the model definitions from the [Full models configuration](#models) section
4. Set `provider` to `"google"` and choose a model
### Verification
```bash
opencode run "Hello" --model=google/antigravity-claude-opus-4-6-thinking --variant=max
```
</details>
---
## Models
### Model Reference
**Antigravity quota** (default routing for Claude and Gemini):
| Model | Variants | Notes |
|-------|----------|-------|
| `antigravity-gemini-3-pro` | low, high | Gemini 3 Pro with thinking |
| `antigravity-gemini-3.1-pro` | low, high | Gemini 3.1 Pro with thinking (rollout-dependent) |
| `antigravity-gemini-3-flash` | minimal, low, medium, high | Gemini 3 Flash with thinking |
| `antigravity-claude-sonnet-4-6` | — | Claude Sonnet 4.6 |
| `antigravity-claude-opus-4-6-thinking` | low, max | Claude Opus 4.6 with extended thinking |
**Gemini CLI quota** (separate from Antigravity; used when `cli_first` is true or as fallback):
| Model | Notes |
|-------|-------|
| `gemini-2.5-flash` | Gemini 2.5 Flash |
| `gemini-2.5-pro` | Gemini 2.5 Pro |
| `gemini-3-flash-preview` | Gemini 3 Flash (preview) |
| `gemini-3-pro-preview` | Gemini 3 Pro (preview) |
| `gemini-3.1-pro-preview` | Gemini 3.1 Pro (preview, rollout-dependent) |
| `gemini-3.1-pro-preview-customtools` | Gemini 3.1 Pro Preview Custom Tools (preview, rollout-dependent) |
> **Routing Behavior:**
> - **Antigravity-first (default):** Gemini models use Antigravity quota across accounts.
> - **CLI-first (`cli_first: true`):** Gemini models use Gemini CLI quota first.
> - When a Gemini quota pool is exhausted, the plugin automatically falls back to the other pool.
> - Claude and image models always use Antigravity.
> Model names are automatically transformed for the target API (e.g., `antigravity-gemini-3-flash` → `gemini-3-flash-preview` for CLI).
**Using variants:**
```bash
opencode run "Hello" --model=google/antigravity-claude-opus-4-6-thinking --variant=max
```
For details on variant configuration and thinking levels, see [docs/MODEL-VARIANTS.md](docs/MODEL-VARIANTS.md).
<details>
<summary><b>Full models configuration (copy-paste ready)</b></summary>
Add this to your `~/.config/opencode/opencode.json`:
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-antigravity-auth@latest"],
"provider": {
"google": {
"models": {
"antigravity-gemini-3-pro": {
"name": "Gemini 3 Pro (Antigravity)",
"limit": { "context": 1048576, "output": 65535 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"variants": {
"low": { "thinkingLevel": "low" },
"high": { "thinkingLevel": "high" }
}
},
"antigravity-gemini-3.1-pro": {
"name": "Gemini 3.1 Pro (Antigravity)",
"limit": { "context": 1048576, "output": 65535 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"variants": {
"low": { "thinkingLevel": "low" },
"high": { "thinkingLevel": "high" }
}
},
"antigravity-gemini-3-flash": {
"name": "Gemini 3 Flash (Antigravity)",
"limit": { "context": 1048576, "output": 65536 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"variants": {
"minimal": { "thinkingLevel": "minimal" },
"low": { "thinkingLevel": "low" },
"medium": { "thinkingLevel": "medium" },
"high": { "thinkingLevel": "high" }
}
},
"antigravity-claude-sonnet-4-6": {
"name": "Claude Sonnet 4.6 (Antigravity)",
"limit": { "context": 200000, "output": 64000 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
},
"antigravity-claude-opus-4-6-thinking": {
"name": "Claude Opus 4.6 Thinking (Antigravity)",
"limit": { "context": 200000, "output": 64000 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"variants": {
"low": { "thinkingConfig": { "thinkingBudget": 8192 } },
"max": { "thinkingConfig": { "thinkingBudget": 32768 } }
}
},
"gemini-2.5-flash": {
"name": "Gemini 2.5 Flash (Gemini CLI)",
"limit": { "context": 1048576, "output": 65536 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
},
"gemini-2.5-pro": {
"name": "Gemini 2.5 Pro (Gemini CLI)",
"limit": { "context": 1048576, "output": 65536 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
},
"gemini-3-flash-preview": {
"name": "Gemini 3 Flash Preview (Gemini CLI)",
"limit": { "context": 1048576, "output": 65536 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
},
"gemini-3-pro-preview": {
"name": "Gemini 3 Pro Preview (Gemini CLI)",
"limit": { "context": 1048576, "output": 65535 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
},
"gemini-3.1-pro-preview": {
"name": "Gemini 3.1 Pro Preview (Gemini CLI)",
"limit": { "context": 1048576, "output": 65535 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
},
"gemini-3.1-pro-preview-customtools": {
"name": "Gemini 3.1 Pro Preview Custom Tools (Gemini CLI)",
"limit": { "context": 1048576, "output": 65535 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
}
}
}
}
}
```
> **Backward Compatibility:** Legacy model names with `antigravity-` prefix (e.g., `antigravity-gemini-3-flash`) still work. The plugin automatically handles model name transformation for both Antigravity and Gemini CLI APIs.
</details>
---
## Multi-Account Setup
Add multiple Google accounts for higher combined quotas. The plugin automatically rotates between accounts when one is rate-limited.
```bash
opencode auth login # Run again to add more accounts
```
**Account management options (via `opencode auth login`):**
- **Configure models** — Auto-configure all plugin models in opencode.json
- **Check quotas** — View remaining API quota for each account
- **Manage accounts** — Enable/disable specific accounts for rotation
For details on load balancing, dual quota pools, and account storage, see [docs/MULTI-ACCOUNT.md](docs/MULTI-ACCOUNT.md).
---
## Troubleshoot
> **Quick Reset**: Most issues can be resolved by deleting `~/.config/opencode/antigravity-accounts.json` and running `opencode auth login` again.
### Configuration Path (All Platforms)
OpenCode uses `~/.config/opencode/` on **all platforms** including Windows.
| File | Path |
|------|------|
| Main config | `~/.config/opencode/opencode.json` |
| Accounts | `~/.config/opencode/antigravity-accounts.json` |
| Plugin config | `~/.config/opencode/antigravity.json` |
| Debug logs | `~/.config/opencode/antigravity-logs/` |
> **Windows users**: `~` resolves to your user home directory (e.g., `C:\Users\YourName`). Do NOT use `%APPDATA%`.
> **Custom path**: Set `OPENCODE_CONFIG_DIR` environment variable to use a custom location.
> **Windows migration**: If upgrading from plugin v1.3.x or earlier, the plugin will automatically find your existing config in `%APPDATA%\opencode\` and use it. New installations use `~/.config/opencode/`.
---
### Multi-Account Auth Issues
If you encounter authentication issues with multiple accounts:
1. Delete the accounts file:
```bash
rm ~/.config/opencode/antigravity-accounts.json
```
2. Re-authenticate:
```bash
opencode auth login
```
---
### 403 Permission Denied (`rising-fact-p41fc`)
**Error:**
```
Permission 'cloudaicompanion.companions.generateChat' denied on resource
'//cloudaicompanion.googleapis.com/projects/rising-fact-p41fc/locations/global'
```
**Cause:** Plugin falls back to a default project ID when no valid project is found. This works for Antigravity but fails for Gemini CLI models.
**Solution:**
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create or select a project
3. Enable the **Gemini for Google Cloud API** (`cloudaicompanion.googleapis.com`)
4. Add `projectId` to your accounts file:
```json
{
"accounts": [
{
"email": "your@email.com",
"refreshToken": "...",
"projectId": "your-project-id"
}
]
}
```
> **Note**: Do this for each account in a multi-account setup.
---
### Gemini Model Not Found
Add this to your `google` provider config:
```json
{
"provider": {
"google": {
"npm": "@ai-sdk/google",
"models": { ... }
}
}
}
```
---
### Gemini 3 Models 400 Error ("Unknown name 'parameters'")
**Error:**
```
Invalid JSON payload received. Unknown name "parameters" at 'request.tools[0]'
```
**Causes:**
- Tool schema incompatibility with Gemini's strict protobuf validation
- MCP servers with malformed schemas
- Plugin version regression
**Solutions:**
1. **Update to latest beta:**
```json
{ "plugin": ["opencode-antigravity-auth@beta"] }
```
2. **Disable MCP servers** one-by-one to find the problematic one
3. **Add npm override:**
```json
{ "provider": { "google": { "npm": "@ai-sdk/google" } } }
```
---
### MCP Servers Causing Errors
Some MCP servers have schemas incompatible with Antigravity's strict JSON format.
**Common symptom:**
```bash
Invalid function name must start with a letter or underscore
```
Sometimes it shows up as:
```bash
GenerateContentRequest.tools[0].function_declarations[12].name: Invalid function name must start with a letter or underscore
```
This usually means an MCP tool name starts with a number (for example, a 1mcp key like `1mcp_*`). Rename the MCP key to start with a letter (e.g., `gw`) or disable that MCP entry for Antigravity models.
**Diagnosis:**
1. Disable all MCP servers in your config
2. Enable one-by-one until error reappears
3. Report the specific MCP in a [GitHub issue](https://github.com/NoeFabris/opencode-antigravity-auth/issues)
---
### "All Accounts Rate-Limited" (But Quota Available)
**Cause:** Cascade bug in `clearExpiredRateLimits()` in hybrid mode (fixed in recent beta).
**Solutions:**
1. Update to latest beta version
2. If persists, delete accounts file and re-authenticate
3. Try switching `account_selection_strategy` to `"sticky"` in `antigravity.json`
---
### Session Recovery
If you encounter errors during a session:
1. Type `continue` to trigger the recovery mechanism
2. If blocked, use `/undo` to revert to pre-error state
3. Retry the operation
---
### Using with Oh-My-OpenCode
**Important:** Disable the built-in Google auth to prevent conflicts:
```json
// ~/.config/opencode/oh-my-opencode.json
{
"google_auth": false,
"agents": {
"frontend-ui-ux-engineer": { "model": "google/antigravity-gemini-3-pro" },
"document-writer": { "model": "google/antigravity-gemini-3-flash" }
}
}
```
---
### Infinite `.tmp` Files Created
**Cause:** When account is rate-limited and plugin retries infinitely, it creates many temp files.
**Workaround:**
1. Stop OpenCode
2. Clean up: `rm ~/.config/opencode/*.tmp`
3. Add more accounts or wait for rate limit to expire
---
### OAuth Callback Issues
<details>
<summary><b>Safari OAuth Callback Fails (macOS)</b></summary>
**Symptoms:**
- "fail to authorize" after successful Google login
- Safari shows "Safari can't open the page"
**Cause:** Safari's "HTTPS-Only Mode" blocks `http://localhost` callback.
**Solutions:**
1. **Use Chrome or Firefox** (easiest):
Copy the OAuth URL and paste into a different browser.
2. **Disable HTTPS-Only Mode temporarily:**
- Safari > Settings (⌘,) > Privacy
- Uncheck "Enable HTTPS-Only Mode"
- Run `opencode auth login`
- Re-enable after authentication
</details>
<details>
<summary><b>Port Conflict (Address Already in Use)</b></summary>
**macOS / Linux:**
```bash
# Find process using the port
lsof -i :51121
# Kill if stale
kill -9 <PID>
# Retry
opencode auth login
```
**Windows (PowerShell):**
```powershell
netstat -ano | findstr :51121
taskkill /PID <PID> /F
opencode auth login
```
</details>
<details>
<summary><b>Docker / WSL2 / Remote Development</b></summary>
OAuth callback requires browser to reach `localhost` on the machine running OpenCode.
**WSL2:**
- Use VS Code's port forwarding, or
- Configure Windows → WSL port forwarding
**SSH / Remote:**
```bash
ssh -L 51121:localhost:51121 user@remote
```
**Docker / Containers:**
- OAuth with localhost redirect doesn't work in containers
- Wait 30s for manual URL flow, or use SSH port forwarding
</details>
---
### Configuration Key Typo: `plugin` not `plugins`
The correct key is `plugin` (singular):
```json
{
"plugin": ["opencode-antigravity-auth@beta"]
}
```
**Not** `"plugins"` (will cause "Unrecognized key" error).
---
### Migrating Accounts Between Machines
When copying `antigravity-accounts.json` to a new machine:
1. Ensure the plugin is installed: `"plugin": ["opencode-antigravity-auth@beta"]`
2. Copy `~/.config/opencode/antigravity-accounts.json`
3. If you get "API key missing" error, the refresh token may be invalid — re-authenticate
## Known Plugin Interactions
For details on load balancing, dual quota pools, and account storage, see [docs/MULTI-ACCOUNT.md](docs/MULTI-ACCOUNT.md).
---
## Plugin Compatibility
### @tarquinen/opencode-dcp
DCP creates synthetic assistant messages that lack thinking blocks. **List this plugin BEFORE DCP:**
```json
{
"plugin": [
"opencode-antigravity-auth@latest",
"@tarquinen/opencode-dcp@latest"
]
}
```
### oh-my-opencode
Disable built-in auth and override agent models in `oh-my-opencode.json`:
```json
{
"google_auth": false,
"agents": {
"frontend-ui-ux-engineer": { "model": "google/antigravity-gemini-3-pro" },
"document-writer": { "model": "google/antigravity-gemini-3-flash" },
"multimodal-looker": { "model": "google/antigravity-gemini-3-flash" }
}
}
```
> **Tip:** When spawning parallel subagents, enable `pid_offset_enabled: true` in `antigravity.json` to distribute sessions across accounts.
### Plugins you don't need
- **gemini-auth plugins** — Not needed. This plugin handles all Google OAuth.
---
## Configuration
Create `~/.config/opencode/antigravity.json` for optional settings:
```json
{
"$schema": "https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/main/assets/antigravity.schema.json"
}
```
Most users don't need to configure anything — defaults work well.
### Model Behavior
| Option | Default | What it does |
|--------|---------|--------------
| `keep_thinking` | `false` | Preserve Claude's thinking across turns. **Warning:** enabling may degrade model stability. |
| `session_recovery` | `true` | Auto-recover from tool errors |
| `cli_first` | `false` | Route Gemini models to Gemini CLI first (Claude and image models stay on Antigravity). |
### Account Rotation
| Your Setup | Recommended Config |
|------------|-------------------|
| **1 account** | `"account_selection_strategy": "sticky"` |
| **2-5 accounts** | Default (`"hybrid"`) works great |
| **5+ accounts** | `"account_selection_strategy": "round-robin"` |
| **Parallel agents** | Add `"pid_offset_enabled": true` |
### Quota Protection
| Option | Default | What it does |
|--------|---------|--------------|
| `soft_quota_threshold_percent` | `90` | Skip account when quota usage exceeds this percentage. Prevents Google from penalizing accounts that fully exhaust quota. Set to `100` to disable. |
| `quota_refresh_interval_minutes` | `15` | Background quota refresh interval. After successful API requests, refreshes quota cache if older than this interval. Set to `0` to disable. |
| `soft_quota_cache_ttl_minutes` | `"auto"` | How long quota cache is considered fresh. `"auto"` = max(2 × refresh interval, 10 minutes). Set a number (1-120) for fixed TTL. |
> **How it works**: Quota cache is refreshed automatically after API requests (when older than `quota_refresh_interval_minutes`) and manually via "Check quotas" in `opencode auth login`. The threshold check uses `soft_quota_cache_ttl_minutes` to determine cache freshness - if cache is older, the account is considered "unknown" and allowed (fail-open). When ALL accounts exceed the threshold, the plugin waits for the earliest quota reset time (like rate limit behavior). If wait time exceeds `max_rate_limit_wait_seconds`, it errors immediately.
### Rate Limit Scheduling
Control how the plugin handles rate limits:
| Option | Default | What it does |
|--------|---------|--------------|
| `scheduling_mode` | `"cache_first"` | `"cache_first"` = wait for same account (preserves prompt cache), `"balance"` = switch immediately, `"performance_first"` = round-robin |
| `max_cache_first_wait_seconds` | `60` | Max seconds to wait in cache_first mode before switching accounts |
| `failure_ttl_seconds` | `3600` | Reset failure count after this many seconds (prevents old failures from permanently penalizing accounts) |
**When to use each mode:**
- **cache_first** (default): Best for long conversations. Waits for the same account to recover, preserving your prompt cache.
- **balance**: Best for quick tasks. Switches accounts immediately when rate-limited for maximum availability.
- **performance_first**: Best for many short requests. Distributes load evenly across all accounts.
### App Behavior
| Option | Default | What it does |
|--------|---------|--------------|
| `quiet_mode` | `false` | Hide toast notifications |
| `debug` | `false` | Enable debug file logging (`~/.config/opencode/antigravity-logs/`) |
| `debug_tui` | `false` | Show debug logs in the TUI log panel (independent from `debug`) |
| `auto_update` | `true` | Auto-update plugin |
For all options, see [docs/CONFIGURATION.md](docs/CONFIGURATION.md).
**Environment variables:**
```bash
OPENCODE_CONFIG_DIR=/path/to/config opencode # Custom config directory
OPENCODE_ANTIGRAVITY_DEBUG=1 opencode # Enable debug file logging
OPENCODE_ANTIGRAVITY_DEBUG=2 opencode # Verbose debug file logging
OPENCODE_ANTIGRAVITY_DEBUG_TUI=1 opencode # Enable TUI log panel debug output
```
---
## Troubleshooting
See the full [Troubleshooting Guide](docs/TROUBLESHOOTING.md) for solutions to common issues including:
- Auth problems and token refresh
- "Model not found" errors
- Session recovery
- Gemini CLI permission errors
- Safari OAuth issues
- Plugin compatibility
- Migration guides
---
## Documentation
- [Configuration](docs/CONFIGURATION.md) — All configuration options
- [Multi-Account](docs/MULTI-ACCOUNT.md) — Load balancing, dual quota pools, account storage
- [Model Variants](docs/MODEL-VARIANTS.md) — Thinking budgets and variant system
- [Troubleshooting](docs/TROUBLESHOOTING.md) — Common issues and fixes
- [Architecture](docs/ARCHITECTURE.md) — How the plugin works
- [API Spec](docs/ANTIGRAVITY_API_SPEC.md) — Antigravity API reference
---
## Support
If this plugin helps you, consider supporting its maintenance:
[](https://ko-fi.com/S6S81QBOIR)
---
## Credits
- [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) by [@jenslys](https://github.com/jenslys)
- [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)
## License
MIT License. See [LICENSE](LICENSE) for details.
<details>
<summary><b>Legal</b></summary>
### Intended Use
- Personal / internal development only
- Respect internal quotas and data handling policies
- Not for production services or bypassing intended limits
### Warning
By using this plugin, you acknowledge:
- **Terms of Service risk** — This approach may violate ToS of AI model providers
- **Account risk** — Providers may suspend or ban accounts
- **No guarantees** — APIs may change without notice
- **Assumption of risk** — You assume all legal, financial, and technical risks
### Disclaimer
- Not affiliated with Google. This is an independent open-source project.
- "Antigravity", "Gemini", "Google Cloud", and "Google" are trademarks of Google LLC.
</details>
================================================
FILE: assets/antigravity.schema.json
================================================
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"$schema": {
"type": "string"
},
"quiet_mode": {
"default": false,
"type": "boolean",
"description": "Suppress most toast notifications (rate limit, account switching). Recovery toasts always shown."
},
"toast_scope": {
"default": "root_only",
"type": "string",
"enum": [
"root_only",
"all"
],
"description": "Controls which sessions show toast notifications. 'root_only' (default) shows in root session only, 'all' shows in all sessions."
},
"debug": {
"default": false,
"type": "boolean",
"description": "Enable debug logging to file."
},
"debug_tui": {
"default": false,
"type": "boolean"
},
"log_dir": {
"type": "string",
"description": "Custom directory for debug logs."
},
"keep_thinking": {
"default": false,
"type": "boolean",
"description": "Preserve thinking blocks for Claude models using signature caching. May cause signature errors."
},
"session_recovery": {
"default": true,
"type": "boolean",
"description": "Enable automatic session recovery from tool_result_missing errors."
},
"auto_resume": {
"default": false,
"type": "boolean",
"description": "Automatically send resume prompt after successful recovery."
},
"resume_text": {
"default": "continue",
"type": "string",
"description": "Custom text to send when auto-resuming after recovery."
},
"signature_cache": {
"type": "object",
"properties": {
"enabled": {
"default": true,
"type": "boolean",
"description": "Enable disk caching of thinking block signatures."
},
"memory_ttl_seconds": {
"default": 3600,
"type": "number",
"minimum": 60,
"maximum": 86400,
"description": "In-memory TTL in seconds."
},
"disk_ttl_seconds": {
"default": 172800,
"type": "number",
"minimum": 3600,
"maximum": 604800,
"description": "Disk TTL in seconds."
},
"write_interval_seconds": {
"default": 60,
"type": "number",
"minimum": 10,
"maximum": 600,
"description": "Background write interval in seconds."
}
},
"required": [
"enabled",
"memory_ttl_seconds",
"disk_ttl_seconds",
"write_interval_seconds"
],
"additionalProperties": false,
"description": "Signature cache configuration for persisting thinking block signatures. Only used when keep_thinking is enabled."
},
"empty_response_max_attempts": {
"default": 4,
"type": "number",
"minimum": 1,
"maximum": 10,
"description": "Maximum retry attempts when Antigravity returns an empty response (no candidates)."
},
"empty_response_retry_delay_ms": {
"default": 2000,
"type": "number",
"minimum": 500,
"maximum": 10000,
"description": "Delay in milliseconds between empty response retries."
},
"tool_id_recovery": {
"default": true,
"type": "boolean",
"description": "Enable tool ID orphan recovery. Matches mismatched tool responses by function name or creates placeholders."
},
"claude_tool_hardening": {
"default": true,
"type": "boolean",
"description": "Enable tool hallucination prevention for Claude models. Injects parameter signatures and strict usage rules."
},
"claude_prompt_auto_caching": {
"default": false,
"type": "boolean",
"description": "Enable Claude prompt auto-caching by adding top-level cache_control when absent."
},
"proactive_token_refresh": {
"default": true,
"type": "boolean",
"description": "Enable proactive background token refresh before expiry, ensuring requests never block."
},
"proactive_refresh_buffer_seconds": {
"default": 1800,
"type": "number",
"minimum": 60,
"maximum": 7200,
"description": "Seconds before token expiry to trigger proactive refresh."
},
"proactive_refresh_check_interval_seconds": {
"default": 300,
"type": "number",
"minimum": 30,
"maximum": 1800,
"description": "Interval between proactive refresh checks in seconds."
},
"max_rate_limit_wait_seconds": {
"default": 300,
"type": "number",
"minimum": 0,
"maximum": 3600
},
"quota_fallback": {
"default": false,
"type": "boolean",
"description": "Deprecated: accepted for backward compatibility but ignored at runtime. Gemini fallback between Antigravity and Gemini CLI is always enabled."
},
"cli_first": {
"default": false,
"type": "boolean",
"description": "Prefer gemini-cli routing before Antigravity for Gemini models. When false (default), Antigravity is tried first and gemini-cli is fallback."
},
"account_selection_strategy": {
"default": "hybrid",
"type": "string",
"enum": [
"sticky",
"round-robin",
"hybrid"
]
},
"pid_offset_enabled": {
"default": false,
"type": "boolean"
},
"switch_on_first_rate_limit": {
"default": true,
"type": "boolean"
},
"scheduling_mode": {
"default": "cache_first",
"type": "string",
"enum": [
"cache_first",
"balance",
"performance_first"
],
"description": "Rate limit scheduling strategy. 'cache_first' (default) waits for cooldowns, 'balance' distributes across accounts, 'performance_first' picks fastest available."
},
"max_cache_first_wait_seconds": {
"default": 60,
"type": "number",
"minimum": 5,
"maximum": 300,
"description": "Maximum seconds to wait for a rate-limited account in cache_first mode before switching."
},
"failure_ttl_seconds": {
"default": 3600,
"type": "number",
"minimum": 60,
"maximum": 7200,
"description": "Time in seconds before a failed account is eligible for retry."
},
"default_retry_after_seconds": {
"default": 60,
"type": "number",
"minimum": 1,
"maximum": 300
},
"max_backoff_seconds": {
"default": 60,
"type": "number",
"minimum": 5,
"maximum": 300
},
"request_jitter_max_ms": {
"default": 0,
"type": "number",
"minimum": 0,
"maximum": 5000,
"description": "Maximum random jitter in milliseconds added to outgoing requests to avoid thundering herd."
},
"soft_quota_threshold_percent": {
"default": 90,
"type": "number",
"minimum": 1,
"maximum": 100,
"description": "Percentage of quota usage that triggers soft quota warnings and preemptive account switching."
},
"quota_refresh_interval_minutes": {
"default": 15,
"type": "number",
"minimum": 0,
"maximum": 60,
"description": "Interval in minutes between quota usage checks. Set to 0 to disable periodic checks."
},
"soft_quota_cache_ttl_minutes": {
"default": "auto",
"anyOf": [
{
"type": "string",
"const": "auto"
},
{
"type": "number",
"minimum": 1,
"maximum": 120
}
],
"description": "TTL for cached soft quota data. 'auto' (default) calculates from refresh interval, or set a fixed number of minutes."
},
"health_score": {
"type": "object",
"properties": {
"initial": {
"default": 70,
"type": "number",
"minimum": 0,
"maximum": 100
},
"success_reward": {
"default": 1,
"type": "number",
"minimum": 0,
"maximum": 10
},
"rate_limit_penalty": {
"default": -10,
"type": "number",
"minimum": -50,
"maximum": 0
},
"failure_penalty": {
"default": -20,
"type": "number",
"minimum": -100,
"maximum": 0
},
"recovery_rate_per_hour": {
"default": 2,
"type": "number",
"minimum": 0,
"maximum": 20
},
"min_usable": {
"default": 50,
"type": "number",
"minimum": 0,
"maximum": 100
},
"max_score": {
"default": 100,
"type": "number",
"minimum": 50,
"maximum": 100
}
},
"required": [
"initial",
"success_reward",
"rate_limit_penalty",
"failure_penalty",
"recovery_rate_per_hour",
"min_usable",
"max_score"
],
"additionalProperties": false
},
"token_bucket": {
"type": "object",
"properties": {
"max_tokens": {
"default": 50,
"type": "number",
"minimum": 1,
"maximum": 1000
},
"regeneration_rate_per_minute": {
"default": 6,
"type": "number",
"minimum": 0.1,
"maximum": 60
},
"initial_tokens": {
"default": 50,
"type": "number",
"minimum": 1,
"maximum": 1000
}
},
"required": [
"max_tokens",
"regeneration_rate_per_minute",
"initial_tokens"
],
"additionalProperties": false
},
"auto_update": {
"default": true,
"type": "boolean",
"description": "Enable automatic plugin updates."
}
},
"additionalProperties": false
}
================================================
FILE: docs/ANTIGRAVITY_API_SPEC.md
================================================
# Antigravity Unified Gateway API Specification
**Version:** 1.0
**Last Updated:** December 13, 2025
**Status:** Verified by Direct API Testing
---
## Overview
Antigravity is Google's **Unified Gateway API** for accessing multiple AI models (Claude, Gemini, GPT-OSS) through a single, consistent Gemini-style interface. It is NOT the same as Vertex AI's direct model APIs.
### Key Characteristics
- **Single API format** for all models (Gemini-style)
- **Project-based access** via Google Cloud authentication
- **Internal routing** to model backends (Vertex AI for Claude, Gemini API for Gemini)
- **Unified response format** (`candidates[]` structure for all models)
---
## Endpoints
| Environment | URL | Status |
|-------------|-----|--------|
| **Daily (Sandbox)** | `https://daily-cloudcode-pa.sandbox.googleapis.com` | ✅ Active |
| **Production** | `https://cloudcode-pa.googleapis.com` | ✅ Active |
| **Autopush (Sandbox)** | `https://autopush-cloudcode-pa.sandbox.googleapis.com` | ❌ Unavailable |
### API Actions
| Action | Path | Description |
|--------|------|-------------|
| Generate Content | `/v1internal:generateContent` | Non-streaming request |
| Stream Generate | `/v1internal:streamGenerateContent?alt=sse` | Streaming (SSE) request |
| Load Code Assist | `/v1internal:loadCodeAssist` | Project discovery |
| Onboard User | `/v1internal:onboardUser` | User onboarding |
---
## Authentication
### OAuth 2.0 Setup
```
Authorization URL: https://accounts.google.com/o/oauth2/auth
Token URL: https://oauth2.googleapis.com/token
```
### Required Scopes
```
https://www.googleapis.com/auth/cloud-platform
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
https://www.googleapis.com/auth/cclog
https://www.googleapis.com/auth/experimentsandconfigs
```
### Required Headers
```http
Authorization: Bearer {access_token}
Content-Type: application/json
User-Agent: antigravity/1.15.8 windows/amd64
X-Goog-Api-Client: google-cloud-sdk vscode_cloudshelleditor/0.1
Client-Metadata: {"ideType":"ANTIGRAVITY","platform":"MACOS","pluginType":"GEMINI"}
```
For streaming requests, also include:
```http
Accept: text/event-stream
```
---
## Available Models
| Model Name | Model ID | Type | Status |
|------------|----------|------|--------|
| Claude Sonnet 4.6 | `claude-sonnet-4-6` | Anthropic | ✅ Verified |
| Claude Opus 4.6 Thinking | `claude-opus-4-6-thinking` | Anthropic | ✅ Verified |
| Gemini 3 Pro High | `gemini-3-pro-high` | Google | ✅ Verified |
| Gemini 3 Pro Low | `gemini-3-pro-low` | Google | ✅ Verified |
| GPT-OSS 120B Medium | `gpt-oss-120b-medium` | Other | ✅ Verified |
---
## Request Format
### Basic Structure
```json
{
"project": "{project_id}",
"model": "{model_id}",
"request": {
"contents": [...],
"generationConfig": {...},
"systemInstruction": {...},
"tools": [...]
},
"userAgent": "antigravity",
"requestId": "{unique_id}"
}
```
### Contents Array (REQUIRED)
**⚠️ IMPORTANT: Must use Gemini-style format. Anthropic-style `messages` array is NOT supported.**
```json
{
"contents": [
{
"role": "user",
"parts": [
{ "text": "Your message here" }
]
},
{
"role": "model",
"parts": [
{ "text": "Assistant response" }
]
}
]
}
```
#### Role Values
- `user` - Human/user messages
- `model` - Assistant responses (NOT `assistant`)
### Generation Config
```json
{
"generationConfig": {
"maxOutputTokens": 1000,
"temperature": 0.7,
"topP": 0.95,
"topK": 40,
"stopSequences": ["STOP"],
"thinkingConfig": {
"thinkingBudget": 8000,
"includeThoughts": true
}
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `maxOutputTokens` | number | Maximum tokens in response |
| `temperature` | number | Randomness (0.0 - 2.0) |
| `topP` | number | Nucleus sampling threshold |
| `topK` | number | Top-K sampling |
| `stopSequences` | string[] | Stop generation triggers |
| `thinkingConfig` | object | Extended thinking config |
### System Instructions
**⚠️ Must be an object with `parts`, NOT a plain string.**
```json
// ✅ CORRECT
{
"systemInstruction": {
"parts": [
{ "text": "You are a helpful assistant." }
]
}
}
// ❌ WRONG - Will return 400 error
{
"systemInstruction": "You are a helpful assistant."
}
```
### Tools / Function Calling
```json
{
"tools": [
{
"functionDeclarations": [
{
"name": "get_weather",
"description": "Get weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name"
}
},
"required": ["location"]
}
}
]
}
]
}
```
### Google Search Grounding
Gemini models support Google Search grounding, but **it cannot be combined with function declarations** in the same request. This plugin implements a dedicated `google_search` tool that makes separate API calls.
#### How the `google_search` Tool Works
The model can call `google_search(query, urls?, thinking?)` which:
1. Makes a **separate API call** to Antigravity with only `{ googleSearch: {} }` (no function declarations)
2. Parses the `groundingMetadata` from the response
3. Returns formatted markdown with sources and citations
**Tool Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | ✅ | The search query or question |
| `urls` | string[] | ❌ | URLs to analyze (adds `urlContext` tool) |
| `thinking` | boolean | ❌ | Enable deep thinking (default: true) |
**Example Response:**
```markdown
## Search Results
Spain won Euro 2024, defeating England 2-1 in the final...
### Sources
- [UEFA Euro 2024](https://uefa.com/...)
- [Al Jazeera](https://aljazeera.com/...)
### Search Queries Used
- "UEFA Euro 2024 winner"
```
#### Raw API Format (for reference)
The underlying API uses these tool formats:
**New API (Gemini 2.0+ / Gemini 3):**
```json
{
"tools": [
{ "googleSearch": {} }
]
}
```
**Legacy API (Gemini 1.5 only - deprecated):**
```json
{
"tools": [
{
"googleSearchRetrieval": {
"dynamicRetrievalConfig": {
"mode": "MODE_DYNAMIC",
"dynamicThreshold": 0.3
}
}
}
]
}
```
**Response includes `groundingMetadata`:**
```json
{
"groundingMetadata": {
"webSearchQueries": ["query1", "query2"],
"searchEntryPoint": { "renderedContent": "..." },
"groundingChunks": [{ "web": { "uri": "...", "title": "..." } }],
"groundingSupports": [{ "segment": {...}, "groundingChunkIndices": [...] }]
}
}
```
> **Important:** `googleSearch` and `urlContext` tools **cannot be combined with `functionDeclarations`** in the same request. This is why the plugin uses a separate API call.
```
### Function Name Rules
| Rule | Description |
|------|-------------|
| First character | Must be a letter (a-z, A-Z) or underscore (_) |
| Allowed characters | `a-zA-Z0-9`, underscores (`_`), dots (`.`), colons (`:`), dashes (`-`) |
| Max length | 64 characters |
| Not allowed | Slashes (`/`), spaces, other special characters |
**Examples:**
- ✅ `get_weather` - Valid
- ✅ `mcp:mongodb.query` - Valid (colons and dots allowed)
- ✅ `read-file` - Valid (dashes allowed)
- ❌ `mcp/query` - Invalid (slashes not allowed)
- ❌ `123_tool` - Invalid (must start with letter or underscore)
### JSON Schema Support
| Feature | Status | Notes |
|---------|--------|-------|
| `type` | ✅ Supported | `object`, `string`, `number`, `integer`, `boolean`, `array` |
| `properties` | ✅ Supported | Object properties |
| `required` | ✅ Supported | Required fields array |
| `description` | ✅ Supported | Field descriptions |
| `enum` | ✅ Supported | Enumerated values |
| `items` | ✅ Supported | Array item schema |
| `anyOf` | ✅ Supported | Converted to `any_of` internally |
| `allOf` | ✅ Supported | Converted to `all_of` internally |
| `oneOf` | ✅ Supported | Converted to `one_of` internally |
| `additionalProperties` | ✅ Supported | Additional properties schema |
| `const` | ❌ NOT Supported | Use `enum: [value]` instead |
| `$ref` | ❌ NOT Supported | Inline the schema instead |
| `$defs` / `definitions` | ❌ NOT Supported | Inline definitions instead |
| `$schema` | ❌ NOT Supported | Strip from schema |
| `$id` | ❌ NOT Supported | Strip from schema |
| `default` | ❌ NOT Supported | Strip from schema |
| `examples` | ❌ NOT Supported | Strip from schema |
| `title` (nested) | ⚠️ Caution | May cause issues in nested objects |
**⚠️ IMPORTANT:** The following features will cause a 400 error if sent to the API:
- `const` - Convert to `enum: [value]` instead
- `$ref` / `$defs` - Inline the schema definitions
- `$schema` / `$id` - Strip these metadata fields
- `default` / `examples` - Strip these documentation fields
```json
// ❌ WRONG - Will return 400 error
{ "type": { "const": "email" } }
// ✅ CORRECT - Use enum instead
{ "type": { "enum": ["email"] } }
```
**Note:** The plugin automatically handles these conversions via the `schema-transform.ts` module.
---
## Response Format
### Non-Streaming Response
```json
{
"response": {
"candidates": [
{
"content": {
"role": "model",
"parts": [
{ "text": "Response text here" }
]
},
"finishReason": "STOP"
}
],
"usageMetadata": {
"promptTokenCount": 16,
"candidatesTokenCount": 4,
"totalTokenCount": 20
},
"modelVersion": "claude-sonnet-4-6",
"responseId": "msg_vrtx_..."
},
"traceId": "abc123..."
}
```
### Streaming Response (SSE)
Content-Type: `text/event-stream`
```
data: {"response": {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hello"}]}}], "usageMetadata": {...}, "modelVersion": "...", "responseId": "..."}, "traceId": "..."}
data: {"response": {"candidates": [{"content": {"role": "model", "parts": [{"text": " world"}]}, "finishReason": "STOP"}], "usageMetadata": {...}}, "traceId": "..."}
```
### Response Fields
| Field | Description |
|-------|-------------|
| `response.candidates` | Array of response candidates |
| `response.candidates[].content.role` | Always `"model"` |
| `response.candidates[].content.parts` | Array of content parts |
| `response.candidates[].finishReason` | `STOP`, `MAX_TOKENS`, `OTHER` |
| `response.usageMetadata.promptTokenCount` | Input tokens |
| `response.usageMetadata.candidatesTokenCount` | Output tokens |
| `response.usageMetadata.totalTokenCount` | Total tokens |
| `response.usageMetadata.thoughtsTokenCount` | Thinking tokens (Gemini) |
| `response.modelVersion` | Actual model used |
| `response.responseId` | Request ID (format varies by model) |
| `traceId` | Trace ID for debugging |
### Response ID Formats
| Model Type | Format | Example |
|------------|--------|---------|
| Claude | `msg_vrtx_...` | `msg_vrtx_01UDKZG8PWPj9mjajje8d7u7` |
| Gemini | Base64-like | `ypM9abPqFKWl0-kPvamgqQw` |
| GPT-OSS | Base64-like | `y5M9aZaSKq6z2roPoJ7pEA` |
---
## Function Call Response
When the model wants to call a function:
```json
{
"response": {
"candidates": [
{
"content": {
"role": "model",
"parts": [
{
"functionCall": {
"name": "get_weather",
"args": {
"location": "Paris"
},
"id": "toolu_vrtx_01PDbPTJgBJ3AJ8BCnSXvUqk"
}
}
]
},
"finishReason": "OTHER"
}
]
}
}
```
### Providing Function Results
```json
{
"contents": [
{ "role": "user", "parts": [{ "text": "What's the weather?" }] },
{ "role": "model", "parts": [{ "functionCall": { "name": "get_weather", "args": {...}, "id": "..." } }] },
{ "role": "user", "parts": [{ "functionResponse": { "name": "get_weather", "id": "...", "response": { "temperature": "22C" } } }] }
]
}
```
---
## Thinking / Extended Reasoning
### Thinking Config
For thinking-capable models (`*-thinking`), use:
```json
{
"generationConfig": {
"maxOutputTokens": 10000,
"thinkingConfig": {
"thinkingBudget": 8000,
"includeThoughts": true
}
}
}
```
**⚠️ IMPORTANT: `maxOutputTokens` must be GREATER than `thinkingBudget`**
### Thinking Response (Gemini)
Gemini models return thinking with signatures:
```json
{
"parts": [
{
"thoughtSignature": "ErADCq0DAXLI2nx...",
"text": "Let me think about this..."
},
{
"text": "The answer is..."
}
]
}
```
### Thinking Response (Claude)
Claude thinking models may include `thought: true` parts:
```json
{
"parts": [
{
"thought": true,
"text": "Reasoning process...",
"thoughtSignature": "..."
},
{
"text": "Final answer..."
}
]
}
```
---
## Error Responses
### Error Structure
```json
{
"error": {
"code": 400,
"message": "Error description",
"status": "INVALID_ARGUMENT",
"details": [...]
}
}
```
### Common Error Codes
| Code | Status | Description |
|------|--------|-------------|
| 400 | `INVALID_ARGUMENT` | Invalid request format |
| 401 | `UNAUTHENTICATED` | Invalid/expired token |
| 403 | `PERMISSION_DENIED` | No access to resource |
| 404 | `NOT_FOUND` | Model not found |
| 429 | `RESOURCE_EXHAUSTED` | Rate limit exceeded |
### Rate Limit Response
```json
{
"error": {
"code": 429,
"message": "You have exhausted your capacity on this model. Your quota will reset after 3s.",
"status": "RESOURCE_EXHAUSTED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.RetryInfo",
"retryDelay": "3.957525076s"
}
]
}
}
```
---
## NOT Supported
The following Anthropic/Vertex AI features are **NOT supported**:
| Feature | Error |
|---------|-------|
| `anthropic_version` | Unknown field |
| `messages` array | Unknown field |
| `max_tokens` | Unknown field |
| Plain string `systemInstruction` | Invalid value |
| `system_instruction` (snake_case at root) | Unknown field |
| JSON Schema `const` | Unknown field (use `enum: [value]`) |
| JSON Schema `$ref` | Not supported (inline instead) |
| JSON Schema `$defs` | Not supported (inline instead) |
| Tool names with `/` | Invalid (use `_` or `:` instead) |
| Tool names starting with digit | Invalid (must start with letter/underscore) |
---
## Complete Request Example
```json
{
"project": "my-project-id",
"model": "claude-sonnet-4-6",
"request": {
"contents": [
{
"role": "user",
"parts": [
{ "text": "Hello, how are you?" }
]
}
],
"systemInstruction": {
"parts": [
{ "text": "You are a helpful assistant." }
]
},
"generationConfig": {
"maxOutputTokens": 1000,
"temperature": 0.7
}
},
"userAgent": "antigravity",
"requestId": "agent-abc123"
}
```
---
## Response Headers
| Header | Description |
|--------|-------------|
| `x-cloudaicompanion-trace-id` | Trace ID for debugging |
| `server-timing` | Request duration |
---
## Comparison: Antigravity vs Vertex AI Anthropic
| Feature | Antigravity | Vertex AI Anthropic |
|---------|-------------|---------------------|
| Endpoint | `cloudcode-pa.googleapis.com` | `aiplatform.googleapis.com` |
| Request format | Gemini-style `contents` | Anthropic `messages` |
| `anthropic_version` | Not used | Required |
| Model names | Simple (`claude-sonnet-4-6`) | Versioned (`claude-4-5@date`) |
| Response format | `candidates[]` | Anthropic `content[]` |
| Multi-model support | Yes (Claude, Gemini, etc.) | Anthropic only |
---
## Changelog
- **2025-12-14**: Added function calling quirks, JSON Schema support matrix, tool name rules
- **2025-12-13**: Initial specification based on direct API testing
================================================
FILE: docs/ARCHITECTURE.md
================================================
# Architecture Guide
**Last Updated:** December 2025
This document explains how the Antigravity plugin works, including the request/response flow, Claude-specific handling, and session recovery.
---
## Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ OpenCode ──▶ Plugin ──▶ Antigravity API ──▶ Claude/Gemini │
│ │ │ │ │ │
│ │ │ │ └─ Model │
│ │ │ └─ Google's gateway (Gemini fmt) │
│ │ └─ THIS PLUGIN (auth, transform, recovery) │
│ └─ AI coding assistant │
└─────────────────────────────────────────────────────────────────┘
```
The plugin intercepts requests to `generativelanguage.googleapis.com`, transforms them for the Antigravity API, and handles authentication, rate limits, and error recovery.
---
## Module Structure
```
src/
├── index.ts # Plugin exports
├── plugin.ts # Main entry, fetch interceptor
├── constants.ts # Endpoints, headers, config
├── antigravity/
│ └── oauth.ts # OAuth token exchange
└── plugin/
├── auth.ts # Token validation & refresh
├── request.ts # Request transformation (main logic)
├── request-helpers.ts # Schema cleaning, thinking filters
├── thinking-recovery.ts # Turn boundary detection, crash recovery
├── recovery.ts # Session recovery (tool_result_missing)
├── quota.ts # Quota checking (API usage stats)
├── cache.ts # Auth & signature caching
├── cache/
│ └── signature-cache.ts # Disk-based signature persistence
├── config/
│ ├── schema.ts # Zod config schema
│ └── loader.ts # Config file loading
├── accounts.ts # Multi-account management
├── server.ts # OAuth callback server
└── debug.ts # Debug logging
```
---
## Request Flow
### 1. Interception (`plugin.ts`)
```typescript
fetch() intercepted → isGenerativeLanguageRequest() → prepareAntigravityRequest()
```
- Account selection (round-robin, rate-limit aware)
- Token refresh if expired
- Endpoint fallback (daily → autopush → prod)
### 2. Request Transformation (`request.ts`)
| Step | What Happens |
|------|--------------|
| Model detection | Detect Claude/Gemini from URL |
| Thinking config | Add `thinkingConfig` for thinking models |
| Thinking strip | Remove ALL thinking blocks (Claude) |
| Tool normalization | Convert to `functionDeclarations[]` |
| Schema cleaning | Remove unsupported JSON Schema fields |
| ID assignment | Assign IDs to tool calls (FIFO matching) |
| Wrap request | `{ project, model, request: {...} }` |
### 3. Response Transformation (`request.ts`)
| Step | What Happens |
|------|--------------|
| SSE streaming | Real-time line-by-line TransformStream |
| Signature caching | Cache `thoughtSignature` for display |
| Format transform | `thought: true` → `type: "reasoning"` |
| Envelope unwrap | Extract inner `response` object |
---
## Claude-Specific Handling
### Why Special Handling?
Claude through Antigravity requires:
1. **Gemini format** - `contents[].parts[]` not `messages[].content[]`
2. **Thinking signatures** - Multi-turn needs signed blocks or errors
3. **Schema restrictions** - Rejects `const`, `$ref`, `$defs`, etc.
4. **Tool validation** - `VALIDATED` mode requires proper schemas
### Thinking Block Strategy (v2.0)
**Problem:** OpenCode stores thinking blocks, but may corrupt signatures.
**Solution:** Strip ALL thinking blocks from outgoing requests.
```
Turn 1 Response: { thought: true, text: "...", thoughtSignature: "abc" }
↓ (stored by OpenCode, possibly corrupted)
Turn 2 Request: Plugin STRIPS all thinking blocks
↓
Claude API: Generates fresh thinking
```
**Why this works:**
- Zero signature errors (impossible to have invalid signatures)
- Same quality (Claude sees full conversation, re-thinks fresh)
- Simpler code (no complex validation/restoration)
### Thinking Injection for Tool Use
Claude API requires thinking before `tool_use` blocks. The plugin:
1. Caches signed thinking from responses (`lastSignedThinkingBySessionKey`)
2. On subsequent requests, injects cached thinking before tool_use
3. Only injects for the **first** assistant message of a turn (not every message)
**Turn boundary detection** (`thinking-recovery.ts`):
```typescript
// A "turn" starts after a real user message (not tool_result)
// Only inject thinking into first assistant message after that
```
---
## Session Recovery
### Tool Result Missing Error
When a tool execution is interrupted (ESC, timeout, crash):
```
Error: tool_use ids were found without tool_result blocks immediately after
```
**Recovery flow** (`recovery.ts`):
1. Detect error via `session.error` event
2. Fetch session messages via `client.session.messages()`
3. Extract `tool_use` IDs from failed message
4. Inject synthetic `tool_result` blocks:
```typescript
{ type: "tool_result", tool_use_id: id, content: "Operation cancelled" }
```
5. Send via `client.session.prompt()`
6. Optionally auto-resume with "continue"
### Thinking Block Order Error
```
Error: Expected thinking but found text
```
**Recovery** (`thinking-recovery.ts`):
1. Detect conversation is in tool loop without thinking at turn start
2. Close the corrupted turn with synthetic messages
3. Start fresh turn where Claude can generate new thinking
---
## Schema Cleaning
Claude rejects unsupported JSON Schema features. The plugin uses an **allowlist approach**:
**Kept:** `type`, `properties`, `required`, `description`, `enum`, `items`
**Removed:** `const`, `$ref`, `$defs`, `default`, `examples`, `additionalProperties`, `$schema`, `title`
**Transformations:**
- `const: "value"` → `enum: ["value"]`
- Empty object schema → Add placeholder `reason` property
---
## Multi-Account Load Balancing
### How It Works
1. **Sticky selection** - Same account until rate limited (preserves cache)
2. **Per-model-family** - Claude/Gemini rate limits tracked separately
3. **Dual quota (Gemini)** - Antigravity + Gemini CLI headers
4. **Automatic failover** - On 429, switch to next available account
### Account Storage
Location: `~/.config/opencode/antigravity-accounts.json`
Contains OAuth refresh tokens - treat as sensitive.
---
## Configuration
### Environment Variables
| Variable | Purpose |
|----------|---------|
| `OPENCODE_ANTIGRAVITY_DEBUG` | `1` or `2` for file debug logging |
| `OPENCODE_ANTIGRAVITY_DEBUG_TUI` | `1` or `true` for TUI log panel debug output |
| `OPENCODE_ANTIGRAVITY_QUIET` | Suppress toast notifications |
`debug` and `debug_tui` are independent sinks: `debug` controls file logs, while `debug_tui` controls TUI logs.
### Config File
Location: `~/.config/opencode/antigravity.json`
```json
{
"session_recovery": true,
"auto_resume": true,
"resume_text": "continue",
"keep_thinking": false
}
```
---
## Key Functions Reference
### `request.ts`
| Function | Purpose |
|----------|---------|
| `prepareAntigravityRequest()` | Main request transformation |
| `transformAntigravityResponse()` | SSE streaming, format conversion |
| `ensureThinkingBeforeToolUseInContents()` | Inject cached thinking |
| `createStreamingTransformer()` | Real-time SSE processing |
### `request-helpers.ts`
| Function | Purpose |
|----------|---------|
| `deepFilterThinkingBlocks()` | Recursive thinking block removal |
| `cleanJSONSchemaForAntigravity()` | Schema sanitization |
| `transformThinkingParts()` | `thought` → `reasoning` format |
### `thinking-recovery.ts`
| Function | Purpose |
|----------|---------|
| `analyzeConversationState()` | Detect turn boundaries, tool loops |
| `needsThinkingRecovery()` | Check if recovery needed |
| `closeToolLoopForThinking()` | Inject synthetic messages |
### `recovery.ts`
| Function | Purpose |
|----------|---------|
| `handleSessionRecovery()` | Main recovery orchestration |
| `createSessionRecoveryHook()` | Hook factory for plugin |
---
## Debugging
### Enable Logging
```bash
export OPENCODE_ANTIGRAVITY_DEBUG=2 # Verbose file logs
export OPENCODE_ANTIGRAVITY_DEBUG_TUI=1 # TUI log panel output
```
### Log Location
`~/.config/opencode/antigravity-logs/`
### What To Check
1. Is `isClaudeModel` true for Claude models?
2. Are thinking blocks being stripped?
3. Are tool schemas being cleaned?
4. Is session recovery triggering?
---
## Troubleshooting
| Error | Cause | Solution |
|-------|-------|----------|
| `invalid signature` | Corrupted thinking block | Update plugin (strips all thinking) |
| `Unknown field: const` | Schema uses `const` | Plugin auto-converts to `enum` |
| `tool_use without tool_result` | Interrupted execution | Session recovery injects results |
| `Expected thinking but found text` | Turn started without thinking | Thinking recovery closes turn |
| `429 Too Many Requests` | Rate limited | Plugin auto-rotates accounts |
---
## See Also
- [ANTIGRAVITY_API_SPEC.md](./ANTIGRAVITY_API_SPEC.md) - API reference
- [README.md](../README.md) - Installation & usage
================================================
FILE: docs/CONFIGURATION.md
================================================
# Configuration
Create `~/.config/opencode/antigravity.json` (or `.opencode/antigravity.json` in project root):
```json
{
"$schema": "https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/main/assets/antigravity.schema.json"
}
```
Most settings have sensible defaults. Only configure what you need.
---
## Quick Start
**Minimal config (recommended for most users):**
```json
{
"$schema": "https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/main/assets/antigravity.schema.json"
}
```
**With web search enabled:**
The plugin provides a `google_search` tool that the model can call to search the web. No configuration is needed - the tool is always available.
---
## Model Behavior
Settings that affect how the model thinks and responds.
| Option | Default | Description |
|--------|---------|-------------|
| `keep_thinking` | `false` | Preserve Claude's thinking blocks across turns. **Warning:** enabling may degrade model stability. |
| `session_recovery` | `true` | Auto-recover from tool_result_missing errors |
| `auto_resume` | `false` | Auto-send resume prompt after recovery |
| `resume_text` | `"continue"` | Text to send when auto-resuming |
> **Note:** The `web_search` config options are deprecated. Google Search is now implemented as a dedicated `google_search` tool that the model can call explicitly.
### About `keep_thinking`
When `true`, Claude's thinking blocks are preserved in conversation history:
- **Pros:** Model remembers its reasoning, more coherent across turns
- **Cons:** May degrade model stability, slightly larger context
When `false` (default), thinking is stripped:
- **Pros:** More stable model behavior, smaller context
- **Cons:** Model may be less coherent, forgets previous reasoning
---
## Account Rotation
Settings for managing multiple Google accounts.
| Option | Default | Description |
|--------|---------|-------------|
| `account_selection_strategy` | `"hybrid"` | How to select accounts |
| `switch_on_first_rate_limit` | `true` | Switch account immediately on first 429 |
| `pid_offset_enabled` | `false` | Distribute sessions across accounts (for parallel agents) |
| `quota_fallback` | `false` | Deprecated (ignored). Kept for backward compatibility; Gemini fallback is automatic |
### Strategy Guide
| Your Setup | Recommended Strategy | Why |
|------------|---------------------|-----|
| **1 account** | `"sticky"` | No rotation needed, preserve prompt cache |
| **2-3 accounts** | `"hybrid"` (default) | Smart rotation with health scoring |
| **4+ accounts** | `"round-robin"` | Maximum throughput |
| **Parallel agents** | `"round-robin"` + `pid_offset_enabled: true` | Distribute across accounts |
### Available Strategies
| Strategy | Behavior | Best For |
|----------|----------|----------|
| `sticky` | Same account until rate-limited | Single account, prompt cache |
| `round-robin` | Rotate on every request | Maximum throughput |
| `hybrid` | Health score + token bucket + LRU | Smart distribution (default) |
---
## App Behavior
Settings for plugin behavior.
| Option | Default | Description |
|--------|---------|-------------|
| `quiet_mode` | `false` | Hide toast notifications (except recovery) |
| `debug` | `false` | Enable debug logging |
| `log_dir` | OS default | Custom directory for debug logs |
| `auto_update` | `true` | Enable automatic plugin updates |
### Debug Logging
```json
{
"debug": true,
"debug_tui": true
}
```
Logs are written to `~/.config/opencode/antigravity-logs/` (or `log_dir` if set).
---
## Recommended Configs
Copy-paste ready configs with all recommended settings enabled.
### 1 Account
```json
{
"$schema": "https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/main/assets/antigravity.schema.json",
"account_selection_strategy": "sticky"
}
```
**Why these settings:**
- `sticky` — No rotation needed, preserves Anthropic prompt cache
### 2-3 Accounts
```json
{
"$schema": "https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/main/assets/antigravity.schema.json",
"account_selection_strategy": "hybrid"
}
```
**Why these settings:**
- `hybrid` — Smart rotation using health scores, avoids bad accounts
### 3+ Accounts (Power Users / Parallel Agents)
```json
{
"$schema": "https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/main/assets/antigravity.schema.json",
"account_selection_strategy": "round-robin",
"switch_on_first_rate_limit": true,
"pid_offset_enabled": true
}
```
**Why these settings:**
- `round-robin` — Maximum throughput, rotates every request
- `switch_on_first_rate_limit` — Immediately switch on 429 (default: true)
- `pid_offset_enabled` — Different sessions use different starting accounts
---
## What's Enabled by Default
These settings are already `true` by default — you don't need to set them:
| Setting | Default | What it does |
|---------|---------|--------------|
| `session_recovery` | `true` | Auto-recover from errors |
| `auto_update` | `true` | Keep plugin updated |
| `switch_on_first_rate_limit` | `true` | Fast account switching |
These settings are `false` by default:
| Setting | Default | What it does |
|---------|---------|--------------|
| `keep_thinking` | `false` | Preserve Claude thinking (may degrade stability) |
| `auto_resume` | `false` | Auto-continue after recovery |
---
## Advanced Settings
> These settings are for edge cases. Most users don't need to change them.
<details>
<summary><b>Error Recovery (internal)</b></summary>
| Option | Default | Description |
|--------|---------|-------------|
| `empty_response_max_attempts` | `4` | Retries for empty API responses |
| `empty_response_retry_delay_ms` | `2000` | Delay between retries |
| `tool_id_recovery` | `true` | Fix mismatched tool IDs from context compaction |
| `claude_tool_hardening` | `true` | Prevent tool parameter hallucination |
| `max_rate_limit_wait_seconds` | `300` | Max wait time when rate limited (0=unlimited) |
</details>
<details>
<summary><b>Token Management (internal)</b></summary>
| Option | Default | Description |
|--------|---------|-------------|
| `proactive_token_refresh` | `true` | Refresh tokens before expiry |
| `proactive_refresh_buffer_seconds` | `1800` | Refresh 30 min before expiry |
| `proactive_refresh_check_interval_seconds` | `300` | Check interval |
</details>
<details>
<summary><b>Signature Cache (internal)</b></summary>
Used when `keep_thinking: true`. Most users don't need to configure this.
| Option | Default | Description |
|--------|---------|-------------|
| `signature_cache.enabled` | `true` | Enable disk caching |
| `signature_cache.memory_ttl_seconds` | `3600` | In-memory cache TTL (1 hour) |
| `signature_cache.disk_ttl_seconds` | `172800` | Disk cache TTL (48 hours) |
| `signature_cache.write_interval_seconds` | `60` | Background write interval |
</details>
<details>
<summary><b>Health Score Tuning (internal)</b></summary>
Used by `hybrid` strategy. Most users don't need to configure this.
| Option | Default | Description |
|--------|---------|-------------|
| `health_score.initial` | `70` | Starting health score |
| `health_score.success_reward` | `1` | Points added on success |
| `health_score.rate_limit_penalty` | `-10` | Points removed on rate limit |
| `health_score.failure_penalty` | `-20` | Points removed on failure |
| `health_score.recovery_rate_per_hour` | `2` | Points recovered per hour |
| `health_score.min_usable` | `50` | Minimum score to use account |
| `health_score.max_score` | `100` | Maximum health score |
</details>
<details>
<summary><b>Token Bucket Tuning (internal)</b></summary>
Used by `hybrid` strategy. Most users don't need to configure this.
| Option | Default | Description |
|--------|---------|-------------|
| `token_bucket.max_tokens` | `50` | Maximum tokens in bucket |
| `token_bucket.regeneration_rate_per_minute` | `6` | Tokens regenerated per minute |
| `token_bucket.initial_tokens` | `50` | Starting tokens |
</details>
================================================
FILE: docs/MODEL-VARIANTS.md
================================================
# Model Variants
OpenCode's variant system allows you to configure thinking budget dynamically instead of defining separate models for each thinking level.
---
## How Variants Work
When you define a model with `variants`, OpenCode shows variant options in the model picker. Selecting a variant passes the `providerOptions` to the plugin, which extracts the thinking configuration.
```bash
opencode run "Hello" --model=google/antigravity-claude-opus-4-6-thinking --variant=max
```
---
## Variant Configuration
Define variants in your model configuration:
```json
{
"antigravity-claude-opus-4-6-thinking": {
"name": "Claude Opus 4.6 Thinking",
"limit": { "context": 200000, "output": 64000 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"variants": {
"low": { "thinkingConfig": { "thinkingBudget": 8192 } },
"max": { "thinkingConfig": { "thinkingBudget": 32768 } }
}
}
}
```
---
## Supported Variant Formats
The plugin accepts different variant formats depending on the model family:
| Model Family | Variant Format | Example |
|--------------|----------------|---------|
| **Claude** | `thinkingConfig.thinkingBudget` | `{ "thinkingConfig": { "thinkingBudget": 8192 } }` |
| **Gemini 3** | `thinkingLevel` | `{ "thinkingLevel": "high" }` |
| **Gemini 2.5** | `thinkingConfig.thinkingBudget` | `{ "thinkingConfig": { "thinkingBudget": 8192 } }` |
---
## Gemini 3 Thinking Levels
Gemini 3 models use string-based thinking levels. Available levels differ by model:
| Level | Flash | Pro | Description |
|-------|-------|-----|-------------|
| `minimal` | ✅ | ❌ | Minimal thinking, lowest latency |
| `low` | ✅ | ✅ | Light thinking |
| `medium` | ✅ | ❌ | Balanced thinking |
| `high` | ✅ | ✅ | Maximum thinking (default) |
> **Note:** The API rejects invalid levels (e.g., `"minimal"` on Pro). Configure variants accordingly.
### Gemini 3 Pro Example
```json
{
"antigravity-gemini-3-pro": {
"name": "Gemini 3 Pro (Antigravity)",
"limit": { "context": 1048576, "output": 65535 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"variants": {
"low": { "thinkingLevel": "low" },
"high": { "thinkingLevel": "high" }
}
}
}
```
### Gemini 3 Flash Example
```json
{
"antigravity-gemini-3-flash": {
"name": "Gemini 3 Flash (Antigravity)",
"limit": { "context": 1048576, "output": 65536 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"variants": {
"minimal": { "thinkingLevel": "minimal" },
"low": { "thinkingLevel": "low" },
"medium": { "thinkingLevel": "medium" },
"high": { "thinkingLevel": "high" }
}
}
}
```
---
## Claude Thinking Budget
Claude models use token-based thinking budgets:
| Variant | Budget | Description |
|---------|--------|-------------|
| `low` | 8192 | Light thinking |
| `max` | 32768 | Maximum thinking |
### Claude Example
```json
{
"antigravity-claude-opus-4-6-thinking": {
"name": "Claude Opus 4.6 Thinking (Antigravity)",
"limit": { "context": 200000, "output": 64000 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"variants": {
"low": { "thinkingConfig": { "thinkingBudget": 8192 } },
"max": { "thinkingConfig": { "thinkingBudget": 32768 } }
}
}
}
```
You can define custom budgets:
```json
{
"variants": {
"minimal": { "thinkingConfig": { "thinkingBudget": 4096 } },
"low": { "thinkingConfig": { "thinkingBudget": 8192 } },
"medium": { "thinkingConfig": { "thinkingBudget": 16384 } },
"high": { "thinkingConfig": { "thinkingBudget": 24576 } },
"max": { "thinkingConfig": { "thinkingBudget": 32768 } }
}
}
```
---
## Legacy Budget Format (Deprecated)
For Gemini 3 models, the old `thinkingBudget` format is still supported but deprecated:
| Budget Range | Maps to Level |
|--------------|---------------|
| ≤ 8192 | low |
| ≤ 16384 | medium |
| > 16384 | high |
**Recommended:** Use `thinkingLevel` directly for Gemini 3 models.
---
## Tier-Suffixed Names
Tier-suffixed model names are still accepted:
- `antigravity-claude-opus-4-6-thinking-low`
- `antigravity-claude-opus-4-6-thinking-medium`
- `antigravity-claude-opus-4-6-thinking-high`
- `antigravity-gemini-3-pro-low`
- `antigravity-gemini-3-pro-high`
- `gemini-3-pro-low`
- `gemini-3-flash-medium`
However, **we recommend using simplified model names with variants** for:
- **Cleaner model picker** — 7 models instead of 12+
- **Simpler config** — No need to configure both `antigravity-` and `-preview` versions
- **Automatic quota routing** — Plugin handles model name transformation
- **Flexible budgets** — Define any budget, not just preset tiers
- **Future-proof** — Works with OpenCode's native variant system
---
## Benefits of Variants
| Before (tier-suffixed) | After (variants) |
|------------------------|------------------|
| 12+ separate models | 4 models with variants |
| Fixed thinking budgets | Customizable budgets |
| Cluttered model picker | Clean model picker |
| Hard to add new tiers | Easy to add new variants |
================================================
FILE: docs/MULTI-ACCOUNT.md
================================================
# Multi-Account Setup
Add multiple Google accounts to increase your combined quota. The plugin automatically rotates between accounts when one is rate-limited.
```bash
opencode auth login # Run again to add more accounts
```
---
## Load Balancing Behavior
- **Sticky account selection** — Sticks to the same account until rate-limited (preserves Anthropic's prompt cache)
- **Per-model-family limits** — Rate limits tracked separately for Claude and Gemini models
- **Antigravity-first for Gemini** — All Gemini requests use Antigravity quota first, then automatically fall back to Gemini CLI when exhausted across all accounts
- **Smart retry threshold** — Short rate limits (≤5s) are retried on same account
- **Exponential backoff** — Increasing delays for consecutive rate limits
---
## Dual Quota Pools
For Gemini models, the plugin accesses **two independent quota pools** per account:
| Quota Pool | When Used |
|------------|-----------|
| **Antigravity** | Default for all requests |
| **Gemini CLI** | Automatic fallback between Antigravity and Gemini CLI in both directions |
This effectively **doubles your Gemini quota** through automatic fallback between Antigravity and Gemini CLI pools.
### How Quota Fallback Works
1. Request uses Antigravity quota on current account
2. If rate-limited, plugin checks if ANY other account has Antigravity available
3. If yes → switch to that account (stay on Antigravity)
4. If no (all accounts exhausted) → fall back to Gemini CLI quota on current account
5. Model names are automatically transformed (e.g., `gemini-3-flash` → `gemini-3-flash-preview`)
Automatic fallback between pools is always enabled for Gemini requests.
---
## Checking Quotas
Check your current API usage across all accounts:
```bash
opencode auth login
# Select "Check quotas" from the menu
```
This shows remaining quota percentages and reset times for each model family:
- **Claude** - Claude Opus/Sonnet quota
- **Gemini 3 Pro** - Gemini 3 Pro quota
- **Gemini 3 Flash** - Gemini 3 Flash quota
### Standalone Quota Script
For checking quotas outside OpenCode (debugging, CI, etc.):
```bash
node scripts/check-quota.mjs # Check all accounts
node scripts/check-quota.mjs --account 2 # Check specific account
node scripts/check-quota.mjs --path /path/to/accounts.json # Custom path
```
---
## Managing Accounts
Enable or disable specific accounts to control which ones are used for requests:
```bash
opencode auth login
# Select "Manage accounts (enable/disable)"
```
Or select an account from the list and choose "Enable/Disable account".
**Disabled accounts:**
- Are excluded from automatic rotation
- Still appear in quota checks (marked `[disabled]`)
- Can be re-enabled at any time
This is useful when:
- An account is temporarily banned or rate-limited for extended periods
- You want to reserve certain accounts for specific use cases
- Testing with a subset of accounts
---
## Adding Accounts
When running `opencode auth login` with existing accounts:
```
2 account(s) saved:
1. user1@gmail.com
2. user2@gmail.com
(a)dd new account(s) or (f)resh start? [a/f]:
```
Choose `a` to add more accounts while keeping existing ones.
---
## Account Storage
Accounts are stored in `~/.config/opencode/antigravity-accounts.json`:
```json
{
"version": 3,
"accounts": [
{
"email": "user1@gmail.com",
"refreshToken": "1//0abc...",
"projectId": "my-gcp-project",
"enabled": true
},
{
"email": "user2@gmail.com",
"refreshToken": "1//0xyz...",
"enabled": false
}
],
"activeIndex": 0,
"activeIndexByFamily": {
"claude": 0,
"gemini": 0
}
}
```
> ⚠️ **Security:** This file contains OAuth refresh tokens. Treat it like a password file.
### Fields
| Field | Description |
|-------|-------------|
| `email` | Google account email |
| `refreshToken` | OAuth refresh token (auto-managed) |
| `projectId` | Optional. Required for Gemini CLI models. See [Troubleshooting](TROUBLESHOOTING.md#gemini-cli-permission-error). |
| `enabled` | Optional. Set to `false` to disable account rotation. Defaults to `true`. |
| `activeIndex` | Currently active account index |
| `activeIndexByFamily` | Per-model-family active account (claude/gemini tracked separately) |
---
## Token Revocation
If Google revokes a token (e.g., password change, security event), you'll see `invalid_grant` errors. The plugin automatically removes invalid accounts.
To manually reset:
```bash
rm ~/.config/opencode/antigravity-accounts.json
opencode auth login
```
---
## Parallel Sessions (oh-my-opencode)
When using oh-my-opencode with parallel subagents, multiple processes may select the same account, causing rate limit errors.
**Solution:** Enable PID-based offset in `antigravity.json`:
```json
{
"pid_offset_enabled": true
}
```
This distributes sessions across accounts based on process ID.
Alternatively, add more accounts via `opencode auth login`.
---
## Account Selection Strategies
Configure in `antigravity.json`:
```json
{
"account_selection_strategy": "hybrid"
}
```
| Strategy | Behavior | Best For |
|----------|----------|----------|
| `sticky` | Same account until rate-limited | Prompt cache preservation |
| `round-robin` | Rotate to next account on every request | Maximum throughput |
| `hybrid` | Deterministic selection based on health score + token bucket + LRU | Best overall distribution |
See [Configuration](CONFIGURATION.md#account-selection) for more details.
================================================
FILE: docs/TROUBLESHOOTING.md
================================================
# Troubleshooting
Common issues and solutions for the Antigravity Auth plugin.
> **Quick Reset**: Most issues can be resolved by deleting `~/.config/opencode/antigravity-accounts.json` and running `opencode auth login` again.
---
## Configuration Paths (All Platforms)
OpenCode uses `~/.config/opencode/` on **all platforms** including Windows.
| File | Path |
|------|------|
| Main config | `~/.config/opencode/opencode.json` |
| Accounts | `~/.config/opencode/antigravity-accounts.json` |
| Plugin config | `~/.config/opencode/antigravity.json` |
| Debug logs | `~/.config/opencode/antigravity-logs/` |
> **Windows users**: `~` resolves to your user home directory (e.g., `C:\Users\YourName`). Do NOT use `%APPDATA%`.
---
## Quick Fixes
### Auth problems
Delete the token file and re-login:
```bash
rm ~/.config/opencode/antigravity-accounts.json
opencode auth login
```
### "This version of Antigravity is no longer supported"
This almost always means an outdated Antigravity `User-Agent` is still being used.
1) Stop any running OpenCode processes (stale processes can overwrite your accounts file):
**macOS/Linux:**
```bash
pkill -f opencode || true
```
**Windows (PowerShell):**
```powershell
Stop-Process -Name "opencode" -Force -ErrorAction SilentlyContinue
```
2) Clear the plugin caches and re-login:
**macOS/Linux:**
```bash
rm -f ~/.config/opencode/antigravity-accounts.json
rm -rf ~/.cache/opencode/node_modules/opencode-antigravity-auth
rm -rf ~/.bun/install/cache/opencode-antigravity-auth*
opencode auth login
```
**Windows (PowerShell):**
```powershell
Remove-Item "$env:APPDATA\opencode\antigravity-accounts.json" -Force -ErrorAction SilentlyContinue
Remove-Item "$env:LOCALAPPDATA\opencode\Cache\node_modules\opencode-antigravity-auth" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item "$env:USERPROFILE\.bun\install\cache\opencode-antigravity-auth*" -Recurse -Force -ErrorAction SilentlyContinue
opencode auth login
```
### "Model not found"
Add this to your `google` provider config:
```json
"npm": "@ai-sdk/google"
```
### Session errors
Type `continue` to trigger auto-recovery, or use `/undo` to rollback.
### Configuration Key Typo
The correct key is `plugin` (singular):
```json
{
"plugin": ["opencode-antigravity-auth@latest"]
}
```
**Not** `"plugins"` (will cause "Unrecognized key" error).
### "Invalid SemVer: beta"
**Error:**
```
Invalid SemVer
{
"name": "UnknownError",
"data": {
"message": "Error: Invalid SemVer: beta ... isOutdated (src/bun/registry.ts:...)"
}
}
```
**Why this happens:** OpenCode's cache may keep the plugin dependency as a dist-tag (`"beta"`) in `~/.cache/opencode/package.json` and `~/.cache/opencode/bun.lock`. Some OpenCode versions compare plugin versions as strict semver and fail on non-numeric tags.
**Fix (recommended):** Re-resolve the dependency in OpenCode cache so it is pinned to a real version.
**macOS / Linux:**
```bash
cd ~/.cache/opencode
bun add opencode-antigravity-auth@latest
```
**Windows (PowerShell):**
```powershell
Set-Location "$env:USERPROFILE\.cache\opencode"
bun add opencode-antigravity-auth@latest
```
Then restart OpenCode.
> If you intentionally run beta channel, use `bun add opencode-antigravity-auth@beta` instead.
---
## Gemini CLI Permission Error
When using Gemini CLI models, you may see:
> Permission 'cloudaicompanion.companions.generateChat' denied on resource '//cloudaicompanion.googleapis.com/projects/...'
**Why this happens:** The plugin defaults to a predefined project ID that doesn't exist in your Google Cloud account. Antigravity models work, but Gemini CLI models need your own project.
**Solution:**
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create or select a project
3. Enable the **Gemini for Google Cloud API** (`cloudaicompanion.googleapis.com`)
4. Add `projectId` to your account in `~/.config/opencode/antigravity-accounts.json`:
```json
{
"version": 3,
"accounts": [
{
"email": "you@gmail.com",
"refreshToken": "...",
"projectId": "your-project-id"
}
]
}
```
> **Note:** For multi-account setups, add `projectId` to each account.
---
## Gemini 3 Models 400 Error ("Unknown name 'parameters'")
**Error:**
```
Invalid JSON payload received. Unknown name "parameters" at 'request.tools[0]'
```
**Causes:**
- Tool schema incompatibility with Gemini's strict protobuf validation
- MCP servers with malformed schemas
- Plugin version regression
**Solutions:**
1. **Update to latest beta:**
```json
{ "plugin": ["opencode-antigravity-auth@beta"] }
```
2. **Disable MCP servers** one-by-one to find the problematic one
3. **Add npm override:**
```json
{ "provider": { "google": { "npm": "@ai-sdk/google" } } }
```
---
## MCP Servers Causing Errors
Some MCP servers have schemas incompatible with Antigravity's strict JSON format.
**Diagnosis:**
1. Disable all MCP servers in your config
2. Enable one-by-one until error reappears
3. Report the specific MCP in a [GitHub issue](https://github.com/NoeFabris/opencode-antigravity-auth/issues)
---
## Rate Limits, Shadow Bans, and Hanging Prompts
**Symptoms:**
- Prompts hang indefinitely (200 OK in logs but no response)
- 403 "Permission Denied" errors even with fresh accounts
- "All accounts rate-limited" but quota looks available
- New accounts get rate-limited immediately after adding
**Why this happens:**
Google has significantly tightened quota and rate-limit enforcement. This affects ALL users, not just this plugin. Key factors:
1. **Stricter enforcement** — Even when quota "looks available," Google may throttle or soft-ban accounts that trigger their abuse detection
2. **OpenCode's request pattern** — OpenCode makes more API calls than native apps (tool calls, retries, streaming, multi-turn chains), which triggers limits faster than "normal" usage
3. **Shadow bans** — Some accounts become effectively unusable for extended periods once flagged, while others continue working normally
> ⚠️ **Important:** Using this plugin may increase the chance of triggering automated abuse/rate-limit protections. The upstream provider can restrict, suspend, or terminate access at their discretion. **USE AT YOUR OWN RISK.**
**Solutions:**
<details>
<summary><b>1. Wait it out (most reliable)</b></summary>
Rate limits typically reset after a few hours. If you're seeing persistent issues:
- Stop using the affected account for 24-48 hours
- Use a different account in the meantime
- Check `rateLimitResetTimes` in your accounts file to see when limits expire
</details>
<details>
<summary><b>2. "Warm up" accounts in Antigravity IDE (community tip)</b></summary>
Users have reported success with this approach:
1. Open [Antigravity IDE](https://idx.google.com/) directly in your browser
2. Log in with the affected Google account
3. Run a few simple prompts (e.g., "Hello", "What's 2+2?")
4. After 5-10 successful prompts, try using the account with the plugin again
**Why this might work:** Using the account through the "official" interface may reset some internal flags or make the account appear less suspicious.
</details>
<details>
<summary><b>3. Reduce request volume and burstiness</b></summary>
- Use shorter sessions
- Avoid parallel/retry-heavy workflows (e.g., spawning many subagents at once)
- If using oh-my-opencode, consider reducing concurrent agent spawns
- Set `max_rate_limit_wait_seconds: 0` to fail fast instead of retrying
</details>
<details>
<summary><b>4. Use Antigravity IDE directly (single account users)</b></summary>
If you only have one account, you'll likely have a better experience using [Antigravity IDE](https://idx.google.com/) directly instead of routing through OpenCode, since OpenCode's request pattern triggers limits faster.
</details>
<details>
<summary><b>5. Fresh account setup</b></summary>
If adding new accounts:
1. Delete accounts file: `rm ~/.config/opencode/antigravity-accounts.json`
2. Re-authenticate: `opencode auth login`
3. Update to latest beta: `"plugin": ["opencode-antigravity-auth@beta"]`
4. Consider "warming up" the account in Antigravity IDE first
</details>
**What to report:**
If you're seeing unusual rate limit behavior, please share in a [GitHub issue](https://github.com/NoeFabris/opencode-antigravity-auth/issues):
- Status codes from debug logs (403, 429, etc.)
- How long the rate-limit state persists
- Number of accounts and selection strategy used
---
## Infinite `.tmp` Files Created
**Cause:** When account is rate-limited and plugin retries infinitely, it creates many temp files.
**Workaround:**
1. Stop OpenCode
2. Clean up: `rm ~/.config/opencode/*.tmp`
3. Add more accounts or wait for rate limit to expire
---
## Safari OAuth Callback Fails (macOS)
**Symptoms:**
- "fail to authorize" after successful Google login
- Safari shows "Safari can't open the page" or connection refused
**Cause:** Safari's "HTTPS-Only Mode" blocks the `http://localhost` callback URL.
**Solutions:**
1. **Use a different browser** (easiest):
Copy the URL from `opencode auth login` and paste it into Chrome or Firefox.
2. **Temporarily disable HTTPS-Only Mode:**
- Safari > Settings (⌘,) > Privacy
- Uncheck "Enable HTTPS-Only Mode"
- Run `opencode auth login`
- Re-enable after authentication
3. **Manual callback extraction** (advanced):
- When Safari shows the error, the address bar contains `?code=...&scope=...`
- See [issue #119](https://github.com/NoeFabris/opencode-antigravity-auth/issues/119) for manual auth support
---
## Port Already in Use
If OAuth fails with "Address already in use":
**macOS / Linux:**
```bash
lsof -i :51121
kill -9 <PID>
opencode auth login
```
**Windows:**
```powershell
netstat -ano | findstr :51121
taskkill /PID <PID> /F
opencode auth login
```
---
## WSL2 / Docker / Remote Development
The OAuth callback requires the browser to reach `localhost` on the machine running OpenCode.
<details>
<summary><b>WSL2</b></summary>
- Use VS Code's port forwarding, or
- Configure Windows → WSL port forwarding
</details>
<details>
<summary><b>SSH / Remote</b></summary>
```bash
ssh -L 51121:localhost:51121 user@remote
```
</details>
<details>
<summary><b>Docker / Containers</b></summary>
- OAuth with localhost redirect doesn't work in containers
- Wait 30s for manual URL flow, or use SSH port forwarding
</details>
---
## Migrating Accounts Between Machines
When copying `antigravity-accounts.json` to a new machine:
1. Ensure the plugin is installed: `"plugin": ["opencode-antigravity-auth@beta"]`
2. Copy `~/.config/opencode/antigravity-accounts.json`
3. If you get "API key missing" error, the refresh token may be invalid — re-authenticate
---
## Plugin Compatibility Issues
### @tarquinen/opencode-dcp
DCP creates synthetic assistant messages that lack thinking blocks. **List this plugin BEFORE DCP:**
```json
{
"plugin": [
"opencode-antigravity-auth@latest",
"@tarquinen/opencode-dcp@latest"
]
}
```
### oh-my-opencode
Disable built-in auth:
```json
{
"google_auth": false
}
```
When spawning parallel subagents, multiple processes may hit the same account. **Workaround:** Enable `pid_offset_enabled: true` or add more accounts.
### Other gemini-auth plugins
You don't need them. This plugin handles all Google OAuth.
---
## Migration Guides
### v1.2.8+ (Variants)
v1.2.8+ introduces **model variants** for dynamic thinking configuration.
**Before (v1.2.7):**
```json
{
"antigravity-claude-opus-4-6-thinking-low": { ... },
"antigravity-claude-opus-4-6-thinking-max": { ... }
}
```
**After (v1.2.8+):**
```json
{
"antigravity-claude-opus-4-6-thinking": {
"variants": {
"low": { "thinkingConfig": { "thinkingBudget": 8192 } },
"max": { "thinkingConfig": { "thinkingBudget": 32768 } }
}
}
}
```
Use canonical model names from current docs. Deprecated model names are sent as requested and may fail if the upstream API has removed them.
### v1.2.7 (Prefix)
v1.2.7+ uses explicit `antigravity-` prefix:
| Old Name | New Name |
|----------|----------|
| `gemini-3-pro-low` | `antigravity-gemini-3-pro` |
| `claude-sonnet-4-6` | `antigravity-claude-sonnet-4-6` |
Use the `antigravity-` prefixed model names shown above.
---
## Debugging
Enable debug logging:
```json
{
"debug": true,
"debug_tui": true
}
```
Logs are in `~/.config/opencode/antigravity-logs/`.
---
## E2E Testing
The plugin includes regression tests (consume API quota):
```bash
npx tsx script/test-regression.ts --sanity # 7 tests, ~5 min
npx tsx script/test-regression.ts --heavy # 4 tests, ~30 min
npx tsx script/test-regression.ts --dry-run # List tests
```
---
## Still stuck?
Open an issue on [GitHub](https://github.com/NoeFabris/opencode-antigravity-auth/issues).
================================================
FILE: index.ts
================================================
export {
AntigravityCLIOAuthPlugin,
GoogleOAuthPlugin,
} from "./src/plugin";
export {
authorizeAntigravity,
exchangeAntigravity,
} from "./src/antigravity/oauth";
export type {
AntigravityAuthorization,
AntigravityTokenExchangeResult,
} from "./src/antigravity/oauth";
================================================
FILE: package.json
================================================
{
"name": "opencode-antigravity-auth",
"version": "1.6.0",
"description": "Google Antigravity IDE OAuth auth plugin for Opencode - access Gemini 3 Pro and Claude 4.6 using Google credentials",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"license": "MIT",
"author": "noefabris",
"repository": {
"type": "git",
"url": "git+https://github.com/NoeFabris/opencode-antigravity-auth.git"
},
"homepage": "https://github.com/NoeFabris/opencode-antigravity-auth#readme",
"bugs": {
"url": "https://github.com/NoeFabris/opencode-antigravity-auth/issues"
},
"keywords": [
"opencode",
"google",
"antigravity",
"gemini",
"oauth",
"plugin",
"auth",
"claude"
],
"engines": {
"node": ">=20.0.0"
},
"files": [
"dist/",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsc -p tsconfig.build.json",
"build:schema": "npx tsx script/build-schema.ts",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"prepublishOnly": "npm run build",
"test:e2e:models": "npx tsx script/test-models.ts",
"test:e2e:regression": "npx tsx script/test-regression.ts"
},
"peerDependencies": {
"typescript": "^5"
},
"devDependencies": {
"@types/node": "^24.10.1",
"@types/proper-lockfile": "^4.1.4",
"@vitest/coverage-v8": "^3.0.0",
"@vitest/ui": "^3.0.0",
"typescript": "^5.0.0",
"vitest": "^3.0.0",
"zod-to-json-schema": "^3.25.1"
},
"dependencies": {
"@opencode-ai/plugin": "^0.15.30",
"@openauthjs/openauth": "^0.4.3",
"proper-lockfile": "^4.1.2",
"xdg-basedir": "^5.1.0",
"zod": "^4.0.0"
}
}
================================================
FILE: script/build-schema.ts
================================================
import { writeFileSync, mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { AntigravityConfigSchema } from "../src/plugin/config/schema.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const outputPath = join(__dirname, "../assets/antigravity.schema.json");
// Use zod v4's built-in toJSONSchema method
const rawSchema = AntigravityConfigSchema.toJSONSchema({
unrepresentable: "any",
override: (_ctx) => undefined // Use default handling
}) as Record<string, unknown>;
// Remove the "required" array since all fields have defaults and are optional
// This preserves backwards compatibility with the draft-07 schema behavior
delete rawSchema.required;
const optionDescriptions: Record<string, string> = {
quiet_mode:
"Suppress most toast notifications (rate limit, account switching). Recovery toasts always shown.",
debug:
"Enable debug logging to file.",
log_dir:
"Custom directory for debug logs.",
keep_thinking:
"Preserve thinking blocks for Claude models using signature caching. May cause signature errors.",
session_recovery:
"Enable automatic session recovery from tool_result_missing errors.",
auto_resume:
"Automatically send resume prompt after successful recovery.",
resume_text:
"Custom text to send when auto-resuming after recovery.",
empty_response_max_attempts:
"Maximum retry attempts when Antigravity returns an empty response (no candidates).",
empty_response_retry_delay_ms:
"Delay in milliseconds between empty response retries.",
tool_id_recovery:
"Enable tool ID orphan recovery. Matches mismatched tool responses by function name or creates placeholders.",
claude_tool_hardening:
"Enable tool hallucination prevention for Claude models. Injects parameter signatures and strict usage rules.",
claude_prompt_auto_caching:
"Enable Claude prompt auto-caching by adding top-level cache_control when absent.",
proactive_token_refresh:
"Enable proactive background token refresh before expiry, ensuring requests never block.",
proactive_refresh_buffer_seconds:
"Seconds before token expiry to trigger proactive refresh.",
proactive_refresh_check_interval_seconds:
"Interval between proactive refresh checks in seconds.",
auto_update: "Enable automatic plugin updates.",
quota_fallback:
"Deprecated: accepted for backward compatibility but ignored at runtime. Gemini fallback between Antigravity and Gemini CLI is always enabled.",
cli_first:
"Prefer gemini-cli routing before Antigravity for Gemini models. When false (default), Antigravity is tried first and gemini-cli is fallback.",
toast_scope:
"Controls which sessions show toast notifications. 'root_only' (default) shows in root session only, 'all' shows in all sessions.",
scheduling_mode:
"Rate limit scheduling strategy. 'cache_first' (default) waits for cooldowns, 'balance' distributes across accounts, 'performance_first' picks fastest available.",
max_cache_first_wait_seconds:
"Maximum seconds to wait for a rate-limited account in cache_first mode before switching.",
failure_ttl_seconds:
"Time in seconds before a failed account is eligible for retry.",
request_jitter_max_ms:
"Maximum random jitter in milliseconds added to outgoing requests to avoid thundering herd.",
soft_quota_threshold_percent:
"Percentage of quota usage that triggers soft quota warnings and preemptive account switching.",
quota_refresh_interval_minutes:
"Interval in minutes between quota usage checks. Set to 0 to disable periodic checks.",
soft_quota_cache_ttl_minutes:
"TTL for cached soft quota data. 'auto' (default) calculates from refresh interval, or set a fixed number of minutes.",
};
const signatureCacheDescriptions: Record<string, string> = {
enabled: "Enable disk caching of thinking block signatures.",
memory_ttl_seconds: "In-memory TTL in seconds.",
disk_ttl_seconds: "Disk TTL in seconds.",
write_interval_seconds: "Background write interval in seconds.",
};
function addDescriptions(schema: Record<string, unknown>): void {
const props = schema.properties as Record<string, Record<string, unknown>> | undefined;
if (!props) return;
for (const [key, prop] of Object.entries(props)) {
if (optionDescriptions[key]) {
prop.description = optionDescriptions[key];
}
if (key === "signature_cache" && prop.properties) {
const cacheProps = prop.properties as Record<string, Record<string, unknown>>;
for (const [cacheKey, cacheProp] of Object.entries(cacheProps)) {
if (signatureCacheDescriptions[cacheKey]) {
cacheProp.description = signatureCacheDescriptions[cacheKey];
}
}
prop.description = "Signature cache configuration for persisting thinking block signatures. Only used when keep_thinking is enabled.";
}
}
}
const definitions = rawSchema.definitions as Record<string, Record<string, unknown>> | undefined;
if (definitions?.AntigravityConfig) {
addDescriptions(definitions.AntigravityConfig);
} else {
addDescriptions(rawSchema);
}
mkdirSync(dirname(outputPath), { recursive: true });
writeFileSync(outputPath, JSON.stringify(rawSchema, null, 2) + "\n");
console.log(`Schema written to ${outputPath}`);
================================================
FILE: script/test-cross-model-e2e.sh
================================================
#!/bin/bash
# Cross-Model E2E Test Suite - 5 Model Variants
# Tests fix for "Invalid `signature` in `thinking` block" error
#
# Models tested:
# 1. Gemini (google/antigravity-gemini-3-pro-low, gemini-3-flash)
# 2. Claude via Anthropic (anthropic/claude-opus-4-5)
# 3. Claude via Google (google/antigravity-claude-*-thinking-*)
# 4. OpenAI (openai/gpt-5.2-medium)
set -euo pipefail
PASS=0
FAIL=0
SKIP=0
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_pass() { echo -e "${GREEN}✓ PASS${NC}: $1"; ((PASS++)); }
log_fail() { echo -e "${RED}✗ FAIL${NC}: $1"; ((FAIL++)); }
log_skip() { echo -e "${YELLOW}○ SKIP${NC}: $1"; ((SKIP++)); }
log_info() { echo -e " ${BLUE}→${NC} $1"; }
get_session_id() {
sleep 1
opencode session list 2>/dev/null | grep -oP 'ses_[a-zA-Z0-9]+' | head -1 || true
}
check_signature_error() {
grep -qi "Invalid.*signature" "$1" 2>/dev/null && return 0 || return 1
}
echo "════════════════════════════════════════════════════════════"
echo " Cross-Model E2E Test Suite - 5 Model Variants"
echo "════════════════════════════════════════════════════════════"
echo ""
# Test 1: Gemini → Anthropic Claude (original bug + direct Anthropic API)
echo "Test 1: Gemini Pro → Anthropic Claude Opus (direct API)"
log_info "Step 1: Gemini with thinking + tool..."
opencode run -m google/antigravity-gemini-3-pro-low \
"Run: echo 'Test1-Gemini'. Think about sequences." \
> /tmp/e2e-t1-s1.log 2>&1 || true
SID=$(get_session_id)
if [ -z "$SID" ]; then
log_fail "Test 1 - No session ID"
else
log_info "Session: $SID"
log_info "Step 2: Anthropic Claude Opus + tool..."
opencode run -s "$SID" -m anthropic/claude-opus-4-5 \
"Run: echo 'Test1-Anthropic-Claude'" \
> /tmp/e2e-t1-s2.log 2>&1 || true
if check_signature_error /tmp/e2e-t1-s2.log; then
log_fail "Test 1 - Invalid signature error (Gemini → Anthropic Claude)"
else
log_pass "Test 1 - Gemini → Anthropic Claude"
fi
fi
echo ""
# Test 2: Gemini → Google Claude (Google-hosted Claude)
echo "Test 2: Gemini Pro → Google Claude Opus Thinking"
log_info "Step 1: Gemini with thinking + tool..."
opencode run -m google/antigravity-gemini-3-pro-low \
"Run: echo 'Test2-Gemini'. Think about this." \
> /tmp/e2e-t2-s1.log 2>&1 || true
SID=$(get_session_id)
if [ -z "$SID" ]; then
log_fail "Test 2 - No session ID"
else
log_info "Session: $SID"
log_info "Step 2: Google Claude Opus Thinking + tool..."
opencode run -s "$SID" -m google/antigravity-claude-opus-4-6-thinking-low \
"Run: echo 'Test2-Google-Claude'" \
> /tmp/e2e-t2-s2.log 2>&1 || true
if check_signature_error /tmp/e2e-t2-s2.log; then
log_fail "Test 2 - Invalid signature error (Gemini → Google Claude)"
else
log_pass "Test 2 - Gemini → Google Claude Thinking"
fi
fi
echo ""
# Test 3: Gemini → OpenAI
echo "Test 3: Gemini Pro → OpenAI GPT-5.2"
log_info "Step 1: Gemini with thinking + tool..."
opencode run -m google/antigravity-gemini-3-pro-low \
"Run: echo 'Test3-Gemini'. Think about AI models." \
> /tmp/e2e-t3-s1.log 2>&1 || true
SID=$(get_session_id)
if [ -z "$SID" ]; then
log_fail "Test 3 - No session ID"
else
log_info "Session: $SID"
log_info "Step 2: OpenAI GPT-5.2 + tool..."
opencode run -s "$SID" -m openai/gpt-5.2-medium \
"Run: echo 'Test3-OpenAI'" \
> /tmp/e2e-t3-s2.log 2>&1 || true
if check_signature_error /tmp/e2e-t3-s2.log; then
log_fail "Test 3 - Invalid signature error (Gemini → OpenAI)"
elif grep -qi "api.*key\|unauthorized\|authentication" /tmp/e2e-t3-s2.log; then
log_skip "Test 3 - OpenAI API key issue (not signature related)"
else
log_pass "Test 3 - Gemini → OpenAI"
fi
fi
echo ""
# Test 4: Anthropic Claude → Gemini (reverse)
echo "Test 4: Anthropic Claude → Gemini (reverse direction)"
log_info "Step 1: Anthropic Claude with tool..."
opencode run -m anthropic/claude-opus-4-5 \
"Run: echo 'Test4-Anthropic-Start'" \
> /tmp/e2e-t4-s1.log 2>&1 || true
SID=$(get_session_id)
if [ -z "$SID" ]; then
log_fail "Test 4 - No session ID"
else
log_info "Session: $SID"
log_info "Step 2: Gemini + thinking + tool..."
opencode run -s "$SID" -m google/antigravity-gemini-3-pro-low \
"Run: echo 'Test4-Gemini'. Think about reversal." \
> /tmp/e2e-t4-s2.log 2>&1 || true
if check_signature_error /tmp/e2e-t4-s2.log; then
log_fail "Test 4 - Invalid signature error (Anthropic Claude → Gemini)"
else
log_pass "Test 4 - Anthropic Claude → Gemini"
fi
fi
echo ""
# Test 5: OpenAI → Google Claude
echo "Test 5: OpenAI → Google Claude Opus Thinking"
log_info "Step 1: OpenAI with tool..."
opencode run -m openai/gpt-5.2-medium \
"Run: echo 'Test5-OpenAI-Start'" \
> /tmp/e2e-t5-s1.log 2>&1 || true
if grep -qi "api.*key\|unauthorized\|authentication" /tmp/e2e-t5-s1.log; then
log_skip "Test 5 - OpenAI API key issue"
else
SID=$(get_session_id)
if [ -z "$SID" ]; then
log_fail "Test 5 - No session ID"
else
log_info "Session: $SID"
log_info "Step 2: Google Claude Opus Thinking + tool..."
opencode run -s "$SID" -m google/antigravity-claude-opus-4-6-thinking-low \
"Run: echo 'Test5-Google-Claude'" \
> /tmp/e2e-t5-s2.log 2>&1 || true
if check_signature_error /tmp/e2e-t5-s2.log; then
log_fail "Test 5 - Invalid signature error (OpenAI → Google Claude)"
else
log_pass "Test 5 - OpenAI → Google Claude"
fi
fi
fi
echo ""
# Test 6: 5-Model Round-Robin (all models in sequence)
echo "Test 6: 5-Model Round-Robin"
log_info "Turn 1: Gemini Pro Low..."
opencode run -m google/antigravity-gemini-3-pro-low \
"Run: echo 'Turn1'. Think about the chain." \
> /tmp/e2e-t6-s1.log 2>&1 || true
SID=$(get_session_id)
if [ -z "$SID" ]; then
log_fail "Test 6 - No session ID"
else
log_info "Session: $SID"
CHAIN_OK=true
log_info "Turn 2: Anthropic Claude..."
opencode run -s "$SID" -m anthropic/claude-opus-4-5 \
"Run: echo 'Turn2'" > /tmp/e2e-t6-s2.log 2>&1 || true
check_signature_error /tmp/e2e-t6-s2.log && CHAIN_OK=false
log_info "Turn 3: Google Claude Opus..."
opencode run -s "$SID" -m google/antigravity-claude-opus-4-6-thinking-low \
"Run: echo 'Turn3'" > /tmp/e2e-t6-s3.log 2>&1 || true
check_signature_error /tmp/e2e-t6-s3.log && CHAIN_OK=false
log_info "Turn 4: OpenAI GPT-5.2..."
opencode run -s "$SID" -m openai/gpt-5.2-medium \
"Run: echo 'Turn4'" > /tmp/e2e-t6-s4.log 2>&1 || true
# Skip OpenAI check if API key issue
if ! grep -qi "api.*key\|unauthorized" /tmp/e2e-t6-s4.log; then
check_signature_error /tmp/e2e-t6-s4.log && CHAIN_OK=false
fi
log_info "Turn 5: Gemini Flash..."
opencode run -s "$SID" -m google/antigravity-gemini-3-flash \
"Run: echo 'Turn5-Complete'" > /tmp/e2e-t6-s5.log 2>&1 || true
check_signature_error /tmp/e2e-t6-s5.log && CHAIN_OK=false
if $CHAIN_OK; then
log_pass "Test 6 - 5-Model Round-Robin"
else
log_fail "Test 6 - 5-Model Round-Robin (signature error in chain)"
fi
fi
echo ""
# Test 7: Google Claude → Anthropic Claude (same family, different API)
echo "Test 7: Google Claude → Anthropic Claude (same family)"
log_info "Step 1: Google Claude Opus Thinking..."
opencode run -m google/antigravity-claude-opus-4-6-thinking-low \
"Run: echo 'Test7-Google-Claude'" \
> /tmp/e2e-t7-s1.log 2>&1 || true
SID=$(get_session_id)
if [ -z "$SID" ]; then
log_fail "Test 7 - No session ID"
else
log_info "Session: $SID"
log_info "Step 2: Anthropic Claude Opus..."
opencode run -s "$SID" -m anthropic/claude-opus-4-5 \
"Run: echo 'Test7-Anthropic-Claude'" \
> /tmp/e2e-t7-s2.log 2>&1 || true
if check_signature_error /tmp/e2e-t7-s2.log; then
log_fail "Test 7 - Invalid signature error (Google Claude → Anthropic Claude)"
else
log_pass "Test 7 - Google Claude → Anthropic Claude"
fi
fi
echo ""
# Test 8: Triple switch with different model families
echo "Test 8: Triple Switch (Gemini → Anthropic → OpenAI)"
log_info "Step 1: Gemini Flash..."
opencode run -m google/antigravity-gemini-3-flash \
"Run: echo 'Triple-1'. Think about it." \
> /tmp/e2e-t8-s1.log 2>&1 || true
SID=$(get_session_id)
if [ -z "$SID" ]; then
log_fail "Test 8 - No session ID"
else
log_info "Session: $SID"
TRIPLE_OK=true
log_info "Step 2: Anthropic Claude..."
opencode run -s "$SID" -m anthropic/claude-opus-4-5 \
"Run: echo 'Triple-2'" > /tmp/e2e-t8-s2.log 2>&1 || true
check_signature_error /tmp/e2e-t8-s2.log && TRIPLE_OK=false
log_info "Step 3: OpenAI..."
opencode run -s "$SID" -m openai/gpt-5.2-medium \
"Run: echo 'Triple-3'" > /tmp/e2e-t8-s3.log 2>&1 || true
if ! grep -qi "api.*key\|unauthorized" /tmp/e2e-t8-s3.log; then
check_signature_error /tmp/e2e-t8-s3.log && TRIPLE_OK=false
fi
if $TRIPLE_OK; then
log_pass "Test 8 - Triple Switch"
else
log_fail "Test 8 - Triple Switch (signature error)"
fi
fi
echo ""
echo "════════════════════════════════════════════════════════════"
echo " Test Results Summary"
echo "════════════════════════════════════════════════════════════"
echo -e " ${GREEN}Passed${NC}: $PASS"
echo -e " ${RED}Failed${NC}: $FAIL"
echo -e " ${YELLOW}Skipped${NC}: $SKIP"
echo ""
if [ $FAIL -gt 0 ]; then
echo -e "${RED}Some tests failed!${NC} Check /tmp/e2e-t*.log for details"
exit 1
else
echo -e "${GREEN}All tests passed!${NC}"
exit 0
fi
================================================
FILE: script/test-cross-model.ts
================================================
#!/usr/bin/env npx tsx
import {
sanitizeCrossModelPayload,
getModelFamily,
} from '../src/plugin/transform/cross-model-sanitizer';
const GEMINI_THOUGHT_SIGNATURE = 'EsgQCsUQAXLI2nybuafAE150LGTo2r78fakesig123abc456def789';
const geminiHistoryWithThinkingAndToolCall = {
contents: [
{
role: 'user',
parts: [{ text: 'Check disk space. Think about which filesystems are most utilized.' }]
},
{
role: 'model',
parts: [
{
thought: true,
text: 'Let me analyze the disk usage by running df -h to see filesystem utilization...',
thoughtSignature: GEMINI_THOUGHT_SIGNATURE
},
{
functionCall: {
name: 'Bash',
args: { command: 'df -h', description: 'Check disk space' }
},
metadata: {
google: {
thoughtSignature: GEMINI_THOUGHT_SIGNATURE
}
}
}
]
},
{
role: 'function',
parts: [{
functionResponse: {
name: 'Bash',
response: {
output: 'Filesystem Size Used Avail Use% Mounted on\n/dev/sda1 100G 62G 38G 62% /'
}
}
}]
},
{
role: 'model',
parts: [{ text: 'The root filesystem is 62% utilized, which is moderate usage.' }]
}
]
};
function runTests(): void {
console.log('=== Cross-Model Sanitization E2E Test ===\n');
let passed = 0;
let failed = 0;
console.log('Test 1: Model family detection');
const geminiFamily = getModelFamily('gemini-3-pro-low');
const claudeFamily = getModelFamily('claude-opus-4-6-thinking-medium');
if (geminiFamily === 'gemini' && claudeFamily === 'claude') {
console.log(' ✅ PASS: Model families detected correctly');
passed++;
} else {
console.log(` ❌ FAIL: Expected gemini/claude, got ${geminiFamily}/${claudeFamily}`);
failed++;
}
console.log('\nTest 2: Gemini → Claude sanitization (exact bug reproduction)');
console.log(' Input: Gemini session with thinking + tool call containing thoughtSignature');
const result = sanitizeCrossModelPayload(geminiHistoryWithThinkingAndToolCall, {
targetModel: 'claude-opus-4-6-thinking-medium'
});
const payload = result.payload as any;
const modelParts = payload.contents[1].parts;
const thinkingPart = modelParts[0];
const toolPart = modelParts[1];
if (thinkingPart.thoughtSignature === undefined) {
console.log(' ✅ PASS: Top-level thoughtSignature stripped from thinking part');
passed++;
} else {
console.log(' ❌ FAIL: thoughtSignature still present on thinking part');
failed++;
}
if (toolPart.metadata?.google?.thoughtSignature === undefined) {
console.log(' ✅ PASS: Nested metadata.google.thoughtSignature stripped from tool part');
passed++;
} else {
console.log(' ❌ FAIL: metadata.google.thoughtSignature still present');
failed++;
}
if (toolPart.functionCall?.name === 'Bash') {
console.log(' ✅ PASS: functionCall structure preserved');
passed++;
} else {
console.log(' ❌ FAIL: functionCall corrupted');
failed++;
}
if (result.modified && result.signaturesStripped === 2) {
console.log(` ✅ PASS: Sanitization metrics correct (modified=true, stripped=${result.signaturesStripped})`);
passed++;
} else {
console.log(` ❌ FAIL: Metrics incorrect (modified=${result.modified}, stripped=${result.signaturesStripped})`);
failed++;
}
console.log('\nTest 3: Same model family - no sanitization');
const sameFamily = sanitizeCrossModelPayload(geminiHistoryWithThinkingAndToolCall, {
targetModel: 'gemini-3-flash'
});
if (!sameFamily.modified && sameFamily.signaturesStripped === 0) {
console.log(' ✅ PASS: No sanitization for same model family');
passed++;
} else {
console.log(' ❌ FAIL: Should not sanitize same model family');
failed++;
}
console.log('\n=== Results ===');
console.log(`Passed: ${passed}/${passed + failed}`);
console.log(`Failed: ${failed}/${passed + failed}`);
if (failed > 0) {
console.log('\n❌ Some tests failed');
process.exit(1);
} else {
console.log('\n✅ All E2E tests passed');
}
}
runTests();
================================================
FILE: script/test-gemini-cli-e2e.sh
================================================
#!/bin/bash
# Gemini CLI E2E Test Suite
# Tests gemini-cli models routing through cloudcode-pa.googleapis.com/v1internal
#
# Models tested:
# 1. google/gemini-2.5-pro
# 2. google/gemini-2.5-flash
# 3. google/gemini-3-pro-preview
# 4. google/gemini-3-flash-preview
set -euo pipefail
PASS=0
FAIL=0
SKIP=0
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_pass() { echo -e "${GREEN}✓ PASS${NC}: $1"; ((PASS++)); }
log_fail() { echo -e "${RED}✗ FAIL${NC}: $1"; ((FAIL++)); }
log_skip() { echo -e "${YELLOW}○ SKIP${NC}: $1"; ((SKIP++)); }
log_info() { echo -e " ${BLUE}→${NC} $1"; }
# Check for common errors
check_auth_error() {
grep -qiE "insufficient.*scope|authentication|unauthorized|403|401" "$1" 2>/dev/null && return 0 || return 1
}
check_quota_error() {
grep -qiE "quota|rate.limit|429|resource.exhausted" "$1" 2>/dev/null && return 0 || return 1
}
check_model_error() {
grep -qiE "model.*not.found|invalid.*model|404" "$1" 2>/dev/null && return 0 || return 1
}
# Test a single model
test_model() {
local model="$1"
local test_name="$2"
local log_file="/tmp/gemini-cli-e2e-${test_name}.log"
log_info "Testing $model..."
# Run opencode with a simple prompt
timeout 60 opencode run -m "$model" \
"Reply with exactly: GEMINI_CLI_OK" \
2>&1 > "$log_file" || true
# Check for various error conditions
if check_auth_error "$log_file"; then
log_fail "$test_name - Authentication/scope error (check OAuth scopes)"
log_info "This likely means routing to wrong endpoint"
return 1
elif check_quota_error "$log_file"; then
log_skip "$test_name - Quota exhausted (not a routing issue)"
return 0
elif check_model_error "$log_file"; then
log_fail "$test_name - Model not found"
return 1
elif grep -qi "GEMINI_CLI_OK\|working\|ok\|hello" "$log_file"; then
log_pass "$test_name"
return 0
elif grep -qi "error\|exception\|failed" "$log_file"; then
log_fail "$test_name - Unknown error"
log_info "Check $log_file for details"
return 1
else
# No obvious error, assume success
log_pass "$test_name"
return 0
fi
}
echo "════════════════════════════════════════════════════════════"
echo " Gemini CLI E2E Test Suite"
echo " Testing cloudcode-pa.googleapis.com/v1internal routing"
echo "════════════════════════════════════════════════════════════"
echo ""
echo "Test 1: google/gemini-2.5-flash"
test_model "google/gemini-2.5-flash" "gemini-2.5-flash" || true
echo ""
echo "Test 2: google/gemini-2.5-pro"
test_model "google/gemini-2.5-pro" "gemini-2.5-pro" || true
echo ""
echo "Test 3: google/gemini-3-flash-preview"
test_model "google/gemini-3-flash-preview" "gemini-3-flash-preview" || true
echo ""
echo "Test 4: google/gemini-3-pro-preview"
test_model "google/gemini-3-pro-preview" "gemini-3-pro-preview" || true
echo ""
# Test 5: Cross-model session (gemini-cli → antigravity)
echo "Test 5: Cross-model session (gemini-cli → antigravity-gemini)"
log_info "Step 1: Start with gemini-2.5-flash..."
timeout 60 opencode run -m google/gemini-2.5-flash \
"Say: SESSION_START" \
2>&1 > /tmp/gemini-cli-e2e-cross-s1.log || true
# Get session ID
sleep 1
SID=$(opencode session list 2>/dev/null | grep -oP 'ses_[a-zA-Z0-9]+' | head -1 || true)
if [ -z "$SID" ]; then
log_fail "Test 5 - No session ID created"
else
log_info "Session: $SID"
log_info "Step 2: Switch to antigravity-gemini-3-flash..."
timeout 60 opencode run -s "$SID" -m google/antigravity-gemini-3-flash \
"Say: SESSION_CONTINUE" \
2>&1 > /tmp/gemini-cli-e2e-cross-s2.log || true
if check_auth_error /tmp/gemini-cli-e2e-cross-s2.log; then
log_fail "Test 5 - Auth error on cross-model switch"
else
log_pass "Test 5 - Cross-model session (gemini-cli → antigravity)"
fi
fi
echo ""
# Test 6: Reverse cross-model (antigravity → gemini-cli)
echo "Test 6: Cross-model session (antigravity → gemini-cli)"
log_info "Step 1: Start with antigravity-gemini-3-pro-low..."
timeout 60 opencode run -m google/antigravity-gemini-3-pro-low \
"Say: ANTIGRAVITY_START" \
2>&1 > /tmp/gemini-cli-e2e-reverse-s1.log || true
sleep 1
SID=$(opencode session list 2>/dev/null | grep -oP 'ses_[a-zA-Z0-9]+' | head -1 || true)
if [ -z "$SID" ]; then
log_fail "Test 6 - No session ID created"
else
log_info "Session: $SID"
log_info "Step 2: Switch to gemini-2.5-pro..."
timeout 60 opencode run -s "$SID" -m google/gemini-2.5-pro \
"Say: GEMINI_CLI_CONTINUE" \
2>&1 > /tmp/gemini-cli-e2e-reverse-s2.log || true
if check_auth_error /tmp/gemini-cli-e2e-reverse-s2.log; then
log_fail "Test 6 - Auth error on reverse cross-model switch"
else
log_pass "Test 6 - Cross-model session (antigravity → gemini-cli)"
fi
fi
echo ""
echo "════════════════════════════════════════════════════════════"
echo " Test Results Summary"
echo "════════════════════════════════════════════════════════════"
echo -e " ${GREEN}Passed${NC}: $PASS"
echo -e " ${RED}Failed${NC}: $FAIL"
echo -e " ${YELLOW}Skipped${NC}: $SKIP"
echo ""
if [ $FAIL -gt 0 ]; then
echo -e "${RED}Some tests failed!${NC}"
echo "Log files: /tmp/gemini-cli-e2e-*.log"
exit 1
else
echo -e "${GREEN}All Gemini CLI tests passed!${NC}"
exit 0
fi
================================================
FILE: script/test-models.ts
================================================
#!/usr/bin/env npx tsx
import { spawn } from "child_process";
interface ModelTest {
model: string;
category: "gemini-cli" | "antigravity-gemini" | "antigravity-claude";
}
const MODELS: ModelTest[] = [
// Gemini CLI (direct Google API)
{ model: "google/gemini-3-flash-preview", category: "gemini-cli" },
{ model: "google/gemini-3-pro-preview", category: "gemini-cli" },
{ model: "google/gemini-2.5-pro", category: "gemini-cli" },
{ model: "google/gemini-2.5-flash", category: "gemini-cli" },
// Antigravity Gemini
{ model: "google/antigravity-gemini-3-pro-low", category: "antigravity-gemini" },
{ model: "google/antigravity-gemini-3-pro-high", category: "antigravity-gemini" },
{ model: "google/antigravity-gemini-3-flash", category: "antigravity-gemini" },
// Antigravity Claude
{ model: "google/antigravity-claude-sonnet-4-6", category: "antigravity-claude" },
{ model: "google/antigravity-claude-opus-4-6-thinking-low", category: "antigravity-claude" },
{ model: "google/antigravity-claude-opus-4-6-thinking-medium", category: "antigravity-claude" },
{ model: "google/antigravity-claude-opus-4-6-thinking-high", category: "antigravity-claude" },
];
const TEST_PROMPT = "Reply with exactly one word: WORKING";
const DEFAULT_TIMEOUT_MS = 120_000;
interface TestResult {
success: boolean;
error?: string;
duration: number;
}
async function testModel(model: string, timeoutMs: number): Promise<TestResult> {
const start = Date.now();
return new Promise((resolve) => {
const proc = spawn("opencode", ["run", TEST_PROMPT, "--model", model], {
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
const timer = setTimeout(() => {
proc.kill("SIGKILL");
resolve({ success: false, error: `Timeout after ${timeoutMs}ms`, duration: Date.now() - start });
}, timeoutMs);
proc.stdout?.on("data", (data) => { stdout += data.toString(); });
proc.stderr?.on("data", (data) => { stderr += data.toString(); });
proc.on("close", (code) => {
clearTimeout(timer);
const duration = Date.now() - start;
if (code !== 0) {
resolve({ success: false, error: `Exit ${code}: ${stderr || stdout}`.slice(0, 200), duration });
} else {
resolve({ success: true, duration });
}
});
proc.on("error", (err) => {
clearTimeout(timer);
resolve({ success: false, error: err.message, duration: Date.now() - start });
});
});
}
function parseArgs(): { filterModel: string | null
gitextract_q9qv3hab/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ └── workflows/ │ ├── ci.yml │ ├── issue-triage.yml │ ├── release-beta.yml │ ├── release.yml │ ├── republish-version.yml │ └── update-dist-tag.yml ├── .gitignore ├── AGENTS.MD ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets/ │ └── antigravity.schema.json ├── docs/ │ ├── ANTIGRAVITY_API_SPEC.md │ ├── ARCHITECTURE.md │ ├── CONFIGURATION.md │ ├── MODEL-VARIANTS.md │ ├── MULTI-ACCOUNT.md │ └── TROUBLESHOOTING.md ├── index.ts ├── package.json ├── script/ │ ├── build-schema.ts │ ├── test-cross-model-e2e.sh │ ├── test-cross-model.ts │ ├── test-gemini-cli-e2e.sh │ ├── test-models.ts │ └── test-regression.ts ├── scripts/ │ ├── README-PI.md │ ├── auth-pi-tools.sh │ ├── check-quota.mjs │ ├── setup-opencode-pi.sh │ └── setup-pi-runner.sh ├── src/ │ ├── antigravity/ │ │ └── oauth.ts │ ├── constants.test.ts │ ├── constants.ts │ ├── hooks/ │ │ └── auto-update-checker/ │ │ ├── cache.ts │ │ ├── checker.ts │ │ ├── constants.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── logging.ts │ │ └── types.ts │ ├── plugin/ │ │ ├── accounts.test.ts │ │ ├── accounts.ts │ │ ├── antigravity-first-fallback.test.ts │ │ ├── auth.test.ts │ │ ├── auth.ts │ │ ├── cache/ │ │ │ ├── index.ts │ │ │ └── signature-cache.ts │ │ ├── cache.test.ts │ │ ├── cache.ts │ │ ├── cli.ts │ │ ├── config/ │ │ │ ├── index.ts │ │ │ ├── loader.ts │ │ │ ├── models.test.ts │ │ │ ├── models.ts │ │ │ ├── schema.test.ts │ │ │ ├── schema.ts │ │ │ ├── updater.test.ts │ │ │ └── updater.ts │ │ ├── core/ │ │ │ └── streaming/ │ │ │ ├── index.ts │ │ │ ├── transformer.ts │ │ │ └── types.ts │ │ ├── cross-model-integration.test.ts │ │ ├── debug.test.ts │ │ ├── debug.ts │ │ ├── errors.ts │ │ ├── fingerprint.ts │ │ ├── image-saver.ts │ │ ├── logger.test.ts │ │ ├── logger.ts │ │ ├── logging-utils.test.ts │ │ ├── logging-utils.ts │ │ ├── model-specific-quota.test.ts │ │ ├── persist-account-pool.test.ts │ │ ├── project.ts │ │ ├── quota-fallback.test.ts │ │ ├── quota.ts │ │ ├── recovery/ │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ ├── storage.ts │ │ │ └── types.ts │ │ ├── recovery.test.ts │ │ ├── recovery.ts │ │ ├── refresh-queue.test.ts │ │ ├── refresh-queue.ts │ │ ├── request-helpers.test.ts │ │ ├── request-helpers.ts │ │ ├── request.test.ts │ │ ├── request.ts │ │ ├── rotation.test.ts │ │ ├── rotation.ts │ │ ├── search.ts │ │ ├── server.ts │ │ ├── storage.test.ts │ │ ├── storage.ts │ │ ├── stores/ │ │ │ └── signature-store.ts │ │ ├── thinking-recovery.ts │ │ ├── token.test.ts │ │ ├── token.ts │ │ ├── transform/ │ │ │ ├── claude.test.ts │ │ │ ├── claude.ts │ │ │ ├── cross-model-sanitizer.test.ts │ │ │ ├── cross-model-sanitizer.ts │ │ │ ├── gemini.test.ts │ │ │ ├── gemini.ts │ │ │ ├── index.ts │ │ │ ├── model-resolver.test.ts │ │ │ ├── model-resolver.ts │ │ │ └── types.ts │ │ ├── types.ts │ │ ├── ui/ │ │ │ ├── ansi.test.ts │ │ │ ├── ansi.ts │ │ │ ├── auth-menu.test.ts │ │ │ ├── auth-menu.ts │ │ │ ├── confirm.ts │ │ │ └── select.ts │ │ ├── version.test.ts │ │ └── version.ts │ ├── plugin.ts │ └── shims.d.ts ├── tsconfig.build.json ├── tsconfig.json └── vitest.config.ts
SYMBOL INDEX (958 symbols across 67 files)
FILE: script/build-schema.ts
function addDescriptions (line 80) | function addDescriptions(schema: Record<string, unknown>): void {
FILE: script/test-cross-model.ts
constant GEMINI_THOUGHT_SIGNATURE (line 7) | const GEMINI_THOUGHT_SIGNATURE = 'EsgQCsUQAXLI2nybuafAE150LGTo2r78fakesi...
function runTests (line 54) | function runTests(): void {
FILE: script/test-models.ts
type ModelTest (line 4) | interface ModelTest {
constant MODELS (line 9) | const MODELS: ModelTest[] = [
constant TEST_PROMPT (line 28) | const TEST_PROMPT = "Reply with exactly one word: WORKING";
constant DEFAULT_TIMEOUT_MS (line 29) | const DEFAULT_TIMEOUT_MS = 120_000;
type TestResult (line 31) | interface TestResult {
function testModel (line 37) | async function testModel(model: string, timeoutMs: number): Promise<Test...
function parseArgs (line 73) | function parseArgs(): { filterModel: string | null; filterCategory: stri...
function printHelp (line 88) | function printHelp(): void {
function main (line 109) | async function main(): Promise<void> {
FILE: script/test-regression.ts
type Category (line 4) | type Category = "thinking-order" | "tool-pairing" | "multi-tool" | "mult...
type TestSuite (line 5) | type TestSuite = "sanity" | "heavy" | "all";
type MultiTurnTest (line 7) | interface MultiTurnTest {
type TurnConfig (line 18) | interface TurnConfig {
type TestResult (line 23) | interface TestResult {
type ConcurrentTest (line 31) | interface ConcurrentTest {
constant ERROR_PATTERNS (line 42) | const ERROR_PATTERNS = [
constant GEMINI_FLASH (line 55) | const GEMINI_FLASH = "google/antigravity-gemini-3-flash";
constant GEMINI_FLASH_CLI_QUOTA (line 56) | const GEMINI_FLASH_CLI_QUOTA = "google/gemini-2.5-flash";
constant CLAUDE_SONNET (line 57) | const CLAUDE_SONNET = "google/antigravity-claude-sonnet-4-6";
constant CLAUDE_OPUS (line 58) | const CLAUDE_OPUS = "google/antigravity-claude-opus-4-6-thinking-low";
constant SANITY_TESTS (line 60) | const SANITY_TESTS: MultiTurnTest[] = [
constant HEAVY_TESTS (line 134) | const HEAVY_TESTS: MultiTurnTest[] = [
function generateEnduranceTest (line 210) | function generateEnduranceTest(turnCount: number): TurnConfig[] {
constant RATE_LIMIT_ERROR_PATTERNS (line 241) | const RATE_LIMIT_ERROR_PATTERNS = [
constant CONCURRENT_TESTS (line 247) | const CONCURRENT_TESTS: ConcurrentTest[] = [
constant ALL_TESTS (line 280) | const ALL_TESTS = [...SANITY_TESTS, ...HEAVY_TESTS];
function runTurn (line 282) | async function runTurn(
function deleteSession (line 346) | async function deleteSession(sessionId: string): Promise<void> {
function runConcurrentTest (line 359) | async function runConcurrentTest(test: ConcurrentTest): Promise<TestResu...
function runMultiTurnTest (line 429) | async function runMultiTurnTest(test: MultiTurnTest): Promise<TestResult> {
function parseArgs (line 490) | function parseArgs(): {
function showHelp (line 516) | function showHelp(): void {
function main (line 555) | async function main(): Promise<void> {
FILE: scripts/check-quota.mjs
constant CLIENT_ID (line 5) | const CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.g...
constant CLIENT_SECRET (line 6) | const CLIENT_SECRET = "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";
constant CLOUD_CODE_BASE (line 7) | const CLOUD_CODE_BASE = "https://cloudcode-pa.googleapis.com";
constant USER_AGENT (line 8) | const USER_AGENT = "antigravity/windows/amd64";
constant FALLBACK_PROJECT_ID (line 9) | const FALLBACK_PROJECT_ID = "bamboo-precept-lgxtn";
function getDefaultAccountsPath (line 11) | function getDefaultAccountsPath() {
function parseArgs (line 20) | function parseArgs() {
function postJson (line 42) | async function postJson(url, token, body, extraHeaders = {}) {
function refreshAccessToken (line 55) | async function refreshAccessToken(refreshToken) {
function loadProjectId (line 78) | async function loadProjectId(accessToken) {
function classifyGroup (line 94) | function classifyGroup(modelName) {
function updateGroup (line 102) | function updateGroup(groups, group, remainingFraction, resetTime) {
function formatDuration (line 128) | function formatDuration(targetTime) {
function printGroup (line 138) | function printGroup(label, entry) {
function run (line 152) | async function run() {
FILE: src/antigravity/oauth.ts
type PkcePair (line 18) | interface PkcePair {
type AntigravityAuthState (line 23) | interface AntigravityAuthState {
type AntigravityAuthorization (line 31) | interface AntigravityAuthorization {
type AntigravityTokenExchangeSuccess (line 37) | interface AntigravityTokenExchangeSuccess {
type AntigravityTokenExchangeFailure (line 46) | interface AntigravityTokenExchangeFailure {
type AntigravityTokenExchangeResult (line 51) | type AntigravityTokenExchangeResult =
type AntigravityTokenResponse (line 55) | interface AntigravityTokenResponse {
type AntigravityUserInfo (line 61) | interface AntigravityUserInfo {
function encodeState (line 68) | function encodeState(payload: AntigravityAuthState): string {
function decodeState (line 75) | function decodeState(state: string): AntigravityAuthState {
function authorizeAntigravity (line 92) | async function authorizeAntigravity(projectId = ""): Promise<Antigravity...
constant FETCH_TIMEOUT_MS (line 116) | const FETCH_TIMEOUT_MS = 10000;
function fetchWithTimeout (line 118) | async function fetchWithTimeout(
function fetchProjectID (line 132) | async function fetchProjectID(accessToken: string): Promise<string> {
function exchangeAntigravity (line 201) | async function exchangeAntigravity(
FILE: src/constants.ts
constant ANTIGRAVITY_CLIENT_ID (line 4) | const ANTIGRAVITY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g...
constant ANTIGRAVITY_CLIENT_SECRET (line 9) | const ANTIGRAVITY_CLIENT_SECRET = "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";
constant ANTIGRAVITY_SCOPES (line 14) | const ANTIGRAVITY_SCOPES: readonly string[] = [
constant ANTIGRAVITY_REDIRECT_URI (line 25) | const ANTIGRAVITY_REDIRECT_URI = "http://localhost:51121/oauth-callback";
constant ANTIGRAVITY_ENDPOINT_DAILY (line 32) | const ANTIGRAVITY_ENDPOINT_DAILY = "https://daily-cloudcode-pa.sandbox.g...
constant ANTIGRAVITY_ENDPOINT_AUTOPUSH (line 33) | const ANTIGRAVITY_ENDPOINT_AUTOPUSH = "https://autopush-cloudcode-pa.san...
constant ANTIGRAVITY_ENDPOINT_PROD (line 34) | const ANTIGRAVITY_ENDPOINT_PROD = "https://cloudcode-pa.googleapis.com";
constant ANTIGRAVITY_ENDPOINT_FALLBACKS (line 40) | const ANTIGRAVITY_ENDPOINT_FALLBACKS = [
constant ANTIGRAVITY_LOAD_ENDPOINTS (line 50) | const ANTIGRAVITY_LOAD_ENDPOINTS = [
constant ANTIGRAVITY_ENDPOINT (line 59) | const ANTIGRAVITY_ENDPOINT = ANTIGRAVITY_ENDPOINT_DAILY;
constant GEMINI_CLI_ENDPOINT (line 66) | const GEMINI_CLI_ENDPOINT = ANTIGRAVITY_ENDPOINT_PROD;
constant ANTIGRAVITY_DEFAULT_PROJECT_ID (line 71) | const ANTIGRAVITY_DEFAULT_PROJECT_ID = "rising-fact-p41fc";
constant ANTIGRAVITY_VERSION_FALLBACK (line 73) | const ANTIGRAVITY_VERSION_FALLBACK = "1.18.3";
function getAntigravityVersion (line 77) | function getAntigravityVersion(): string { return antigravityVersion; }
function setAntigravityVersion (line 83) | function setAntigravityVersion(version: string): void {
constant ANTIGRAVITY_VERSION (line 90) | const ANTIGRAVITY_VERSION = ANTIGRAVITY_VERSION_FALLBACK;
function getAntigravityHeaders (line 92) | function getAntigravityHeaders(): HeaderSet & { "Client-Metadata": strin...
constant ANTIGRAVITY_HEADERS (line 101) | const ANTIGRAVITY_HEADERS = {
constant GEMINI_CLI_HEADERS (line 107) | const GEMINI_CLI_HEADERS = {
constant ANTIGRAVITY_PLATFORMS (line 113) | const ANTIGRAVITY_PLATFORMS = ["windows/amd64", "darwin/arm64", "darwin/...
constant ANTIGRAVITY_API_CLIENTS (line 115) | const ANTIGRAVITY_API_CLIENTS = [
function randomFrom (line 121) | function randomFrom<T>(arr: readonly T[]): T {
type HeaderSet (line 125) | type HeaderSet = {
function getRandomizedHeaders (line 131) | function getRandomizedHeaders(style: HeaderStyle, model?: string): Heade...
type HeaderStyle (line 148) | type HeaderStyle = "antigravity" | "gemini-cli";
constant ANTIGRAVITY_PROVIDER_ID (line 153) | const ANTIGRAVITY_PROVIDER_ID = "google";
constant CLAUDE_TOOL_SYSTEM_INSTRUCTION (line 166) | const CLAUDE_TOOL_SYSTEM_INSTRUCTION = `CRITICAL TOOL USAGE INSTRUCTIONS:
constant CLAUDE_DESCRIPTION_PROMPT (line 183) | const CLAUDE_DESCRIPTION_PROMPT = "\n\n⚠️ STRICT PARAMETERS: {params}.";
constant EMPTY_SCHEMA_PLACEHOLDER_NAME (line 185) | const EMPTY_SCHEMA_PLACEHOLDER_NAME = "_placeholder";
constant EMPTY_SCHEMA_PLACEHOLDER_DESCRIPTION (line 186) | const EMPTY_SCHEMA_PLACEHOLDER_DESCRIPTION = "Placeholder. Always pass t...
constant SKIP_THOUGHT_SIGNATURE (line 201) | const SKIP_THOUGHT_SIGNATURE = "skip_thought_signature_validator";
constant SEARCH_MODEL (line 220) | const SEARCH_MODEL = "gemini-2.5-flash";
constant SEARCH_THINKING_BUDGET_DEEP (line 225) | const SEARCH_THINKING_BUDGET_DEEP = 16384;
constant SEARCH_THINKING_BUDGET_FAST (line 230) | const SEARCH_THINKING_BUDGET_FAST = 4096;
constant SEARCH_TIMEOUT_MS (line 235) | const SEARCH_TIMEOUT_MS = 60000;
constant SEARCH_SYSTEM_INSTRUCTION (line 240) | const SEARCH_SYSTEM_INSTRUCTION = `You are an expert web search assistan...
constant ANTIGRAVITY_SYSTEM_INSTRUCTION (line 254) | const ANTIGRAVITY_SYSTEM_INSTRUCTION = `You are Antigravity, a powerful ...
FILE: src/hooks/auto-update-checker/cache.ts
type BunLockfile (line 5) | interface BunLockfile {
function stripTrailingCommas (line 14) | function stripTrailingCommas(json: string): string {
function removeFromBunLock (line 18) | function removeFromBunLock(packageName: string): boolean {
function invalidatePackage (line 48) | function invalidatePackage(packageName: string = PACKAGE_NAME): boolean {
function invalidateCache (line 88) | function invalidateCache(): boolean {
FILE: src/hooks/auto-update-checker/checker.ts
function isLocalDevMode (line 15) | function isLocalDevMode(directory: string): boolean {
function stripJsonComments (line 19) | function stripJsonComments(json: string): string {
function getConfigPaths (line 25) | function getConfigPaths(directory: string): string[] {
function getLocalDevPath (line 35) | function getLocalDevPath(directory: string): string | null {
function findPackageJsonUp (line 60) | function findPackageJsonUp(startPath: string): string | null {
function getLocalDevVersion (line 86) | function getLocalDevVersion(directory: string): string | null {
type PluginEntryInfo (line 101) | interface PluginEntryInfo {
function findPluginEntry (line 108) | function findPluginEntry(directory: string): PluginEntryInfo | null {
function getCachedVersion (line 137) | function getCachedVersion(): string | null {
function updatePinnedVersion (line 163) | function updatePinnedVersion(configPath: string, oldEntry: string, newVe...
function getLatestVersion (line 213) | async function getLatestVersion(): Promise<string | null> {
function checkForUpdate (line 234) | async function checkForUpdate(directory: string): Promise<UpdateCheckRes...
FILE: src/hooks/auto-update-checker/constants.ts
constant PACKAGE_NAME (line 4) | const PACKAGE_NAME = "opencode-antigravity-auth";
constant NPM_REGISTRY_URL (line 5) | const NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE...
constant NPM_FETCH_TIMEOUT (line 6) | const NPM_FETCH_TIMEOUT = 5000;
function getCacheDir (line 8) | function getCacheDir(): string {
constant CACHE_DIR (line 15) | const CACHE_DIR = getCacheDir();
constant INSTALLED_PACKAGE_JSON (line 16) | const INSTALLED_PACKAGE_JSON = path.join(
function getUserConfigDir (line 23) | function getUserConfigDir(): string {
constant USER_CONFIG_DIR (line 30) | const USER_CONFIG_DIR = getUserConfigDir();
constant USER_OPENCODE_CONFIG (line 31) | const USER_OPENCODE_CONFIG = path.join(USER_CONFIG_DIR, "opencode", "ope...
constant USER_OPENCODE_CONFIG_JSONC (line 32) | const USER_OPENCODE_CONFIG_JSONC = path.join(USER_CONFIG_DIR, "opencode"...
FILE: src/hooks/auto-update-checker/index.test.ts
function createMockClient (line 23) | function createMockClient() {
function createPluginInfo (line 31) | function createPluginInfo(overrides: Partial<ReturnType<typeof findPlugi...
FILE: src/hooks/auto-update-checker/index.ts
type PluginClient (line 7) | interface PluginClient {
type SessionCreatedEvent (line 20) | interface SessionCreatedEvent {
type PluginEvent (line 29) | type PluginEvent = SessionCreatedEvent | { type: string; properties?: un...
function createAutoUpdateCheckerHook (line 31) | function createAutoUpdateCheckerHook(
function runBackgroundUpdateCheck (line 69) | async function runBackgroundUpdateCheck(
function showUpdateAvailableToast (line 126) | async function showUpdateAvailableToast(client: PluginClient, latestVers...
function showAutoUpdatedToast (line 140) | async function showAutoUpdatedToast(client: PluginClient, oldVersion: st...
function showLocalDevToast (line 154) | async function showLocalDevToast(client: PluginClient, version: string):...
FILE: src/hooks/auto-update-checker/logging.ts
constant AUTO_UPDATE_LOG_PREFIX (line 3) | const AUTO_UPDATE_LOG_PREFIX = "[auto-update-checker]";
function formatAutoUpdateLogMessage (line 5) | function formatAutoUpdateLogMessage(message: string): string {
function logAutoUpdate (line 9) | function logAutoUpdate(message: string): void {
FILE: src/hooks/auto-update-checker/types.ts
type NpmDistTags (line 1) | interface NpmDistTags {
type OpencodeConfig (line 6) | interface OpencodeConfig {
type PackageJson (line 11) | interface PackageJson {
type UpdateCheckResult (line 17) | interface UpdateCheckResult {
type AutoUpdateCheckerOptions (line 25) | interface AutoUpdateCheckerOptions {
FILE: src/plugin.ts
constant MAX_OAUTH_ACCOUNTS (line 64) | const MAX_OAUTH_ACCOUNTS = 10;
constant MAX_WARMUP_SESSIONS (line 65) | const MAX_WARMUP_SESSIONS = 1000;
constant MAX_WARMUP_RETRIES (line 66) | const MAX_WARMUP_RETRIES = 2;
constant CAPACITY_BACKOFF_TIERS_MS (line 67) | const CAPACITY_BACKOFF_TIERS_MS = [5000, 10000, 20000, 30000, 60000];
function getCapacityBackoffDelay (line 69) | function getCapacityBackoffDelay(consecutiveFailures: number): number {
constant RATE_LIMIT_TOAST_COOLDOWN_MS (line 85) | const RATE_LIMIT_TOAST_COOLDOWN_MS = 5000;
constant MAX_TOAST_COOLDOWN_ENTRIES (line 86) | const MAX_TOAST_COOLDOWN_ENTRIES = 100;
function cleanupToastCooldowns (line 95) | function cleanupToastCooldowns(): void {
function shouldShowRateLimitToast (line 106) | function shouldShowRateLimitToast(message: string): boolean {
function resetAllAccountsBlockedToasts (line 118) | function resetAllAccountsBlockedToasts(): void {
function triggerAsyncQuotaRefreshForAccount (line 125) | async function triggerAsyncQuotaRefreshForAccount(
function trackWarmupAttempt (line 171) | function trackWarmupAttempt(sessionId: string): boolean {
function getWarmupAttemptCount (line 190) | function getWarmupAttemptCount(sessionId: string): number {
function markWarmupSuccess (line 194) | function markWarmupSuccess(sessionId: string): void {
function clearWarmupAttempt (line 202) | function clearWarmupAttempt(sessionId: string): void {
function isWSL (line 206) | function isWSL(): boolean {
function isWSL2 (line 217) | function isWSL2(): boolean {
function isRemoteEnvironment (line 228) | function isRemoteEnvironment(): boolean {
function shouldSkipLocalServer (line 241) | function shouldSkipLocalServer(): boolean {
function openBrowser (line 245) | async function openBrowser(url: string): Promise<boolean> {
type VerificationProbeResult (line 271) | type VerificationProbeResult = {
function decodeEscapedText (line 277) | function decodeEscapedText(input: string): string {
function normalizeGoogleVerificationUrl (line 283) | function normalizeGoogleVerificationUrl(rawUrl: string): string | undefi...
function selectBestVerificationUrl (line 299) | function selectBestVerificationUrl(urls: string[]): string | undefined {
function extractVerificationErrorDetails (line 318) | function extractVerificationErrorDetails(bodyText: string): {
function verifyAccountAccess (line 438) | async function verifyAccountAccess(
function promptAccountIndexForVerification (line 550) | async function promptAccountIndexForVerification(
function promptOpenVerificationUrl (line 587) | async function promptOpenVerificationUrl(): Promise<boolean> {
type VerificationStoredAccount (line 592) | type VerificationStoredAccount = {
function markStoredAccountVerificationRequired (line 600) | function markStoredAccountVerificationRequired(
function clearStoredAccountVerificationRequired (line 638) | function clearStoredAccountVerificationRequired(
function promptOAuthCallbackValue (line 670) | async function promptOAuthCallbackValue(message: string): Promise<string> {
type OAuthCallbackParams (line 681) | type OAuthCallbackParams = { code: string; state: string };
function getStateFromAuthorizationUrl (line 683) | function getStateFromAuthorizationUrl(authorizationUrl: string): string {
function extractOAuthCallbackParams (line 691) | function extractOAuthCallbackParams(url: URL): OAuthCallbackParams | null {
function parseOAuthCallbackInput (line 700) | function parseOAuthCallbackInput(
function promptManualOAuthInput (line 731) | async function promptManualOAuthInput(
function clampInt (line 749) | function clampInt(value: number, min: number, max: number): number {
function persistAccountPool (line 756) | async function persistAccountPool(
function buildAuthSuccessFromStoredAccount (line 860) | function buildAuthSuccessFromStoredAccount(account: {
function retryAfterMsFromResponse (line 882) | function retryAfterMsFromResponse(response: Response, defaultRetryMs: nu...
function parseDurationToMs (line 909) | function parseDurationToMs(duration: string): number | null {
type RateLimitBodyInfo (line 945) | interface RateLimitBodyInfo {
function extractRateLimitBodyInfo (line 952) | function extractRateLimitBodyInfo(body: unknown): RateLimitBodyInfo {
function extractRetryInfoFromBody (line 1024) | async function extractRetryInfoFromBody(response: Response): Promise<Rat...
function formatWaitTime (line 1038) | function formatWaitTime(ms: number): string {
constant FIRST_RETRY_DELAY_MS (line 1053) | const FIRST_RETRY_DELAY_MS = 1000;
constant SWITCH_ACCOUNT_DELAY_MS (line 1054) | const SWITCH_ACCOUNT_DELAY_MS = 5000;
constant RATE_LIMIT_DEDUP_WINDOW_MS (line 1066) | const RATE_LIMIT_DEDUP_WINDOW_MS = 2000;
constant RATE_LIMIT_STATE_RESET_MS (line 1067) | const RATE_LIMIT_STATE_RESET_MS = 120_000;
type RateLimitState (line 1069) | interface RateLimitState {
function getRateLimitBackoff (line 1090) | function getRateLimitBackoff(
function resetRateLimitState (line 1132) | function resetRateLimitState(accountIndex: number, quotaKey: string): vo...
function resetAllRateLimitStateForAccount (line 1141) | function resetAllRateLimitStateForAccount(accountIndex: number): void {
function headerStyleToQuotaKey (line 1149) | function headerStyleToQuotaKey(headerStyle: HeaderStyle, family: ModelFa...
constant MAX_CONSECUTIVE_FAILURES (line 1156) | const MAX_CONSECUTIVE_FAILURES = 5;
constant FAILURE_COOLDOWN_MS (line 1157) | const FAILURE_COOLDOWN_MS = 30_000;
constant FAILURE_STATE_RESET_MS (line 1158) | const FAILURE_STATE_RESET_MS = 120_000;
function trackAccountFailure (line 1160) | function trackAccountFailure(accountIndex: number): { failures: number; ...
function resetAccountFailureState (line 1177) | function resetAccountFailureState(accountIndex: number): void {
function sleep (line 1184) | function sleep(ms: number, signal?: AbortSignal | null): Promise<void> {
method execute (line 1342) | async execute(args, ctx) {
method fetch (line 1454) | async fetch(input, init) {
function toUrlString (line 3334) | function toUrlString(value: RequestInfo): string {
function toWarmupStreamUrl (line 3345) | function toWarmupStreamUrl(value: RequestInfo): string {
function extractModelFromUrl (line 3359) | function extractModelFromUrl(urlString: string): string | null {
function extractModelFromUrlWithSuffix (line 3364) | function extractModelFromUrlWithSuffix(urlString: string): string | null {
function getModelFamilyFromUrl (line 3369) | function getModelFamilyFromUrl(urlString: string): ModelFamily {
function resolveQuotaFallbackHeaderStyle (line 3381) | function resolveQuotaFallbackHeaderStyle(input: {
type HeaderRoutingDecision (line 3395) | type HeaderRoutingDecision = {
function resolveHeaderRoutingDecision (line 3402) | function resolveHeaderRoutingDecision(
function getCliFirst (line 3418) | function getCliFirst(config: AntigravityConfig): boolean {
function getHeaderStyleFromUrl (line 3422) | function getHeaderStyleFromUrl(
function isExplicitQuotaFromUrl (line 3438) | function isExplicitQuotaFromUrl(urlString: string): boolean {
FILE: src/plugin/accounts.ts
type RateLimitReason (line 17) | type RateLimitReason =
type RateLimitBackoffResult (line 24) | interface RateLimitBackoffResult {
constant QUOTA_EXHAUSTED_BACKOFFS (line 29) | const QUOTA_EXHAUSTED_BACKOFFS = [60_000, 300_000, 1_800_000, 7_200_000]...
constant RATE_LIMIT_EXCEEDED_BACKOFF (line 30) | const RATE_LIMIT_EXCEEDED_BACKOFF = 30_000;
constant MODEL_CAPACITY_EXHAUSTED_BASE_BACKOFF (line 32) | const MODEL_CAPACITY_EXHAUSTED_BASE_BACKOFF = 45_000;
constant MODEL_CAPACITY_EXHAUSTED_JITTER_MAX (line 33) | const MODEL_CAPACITY_EXHAUSTED_JITTER_MAX = 30_000;
constant SERVER_ERROR_BACKOFF (line 34) | const SERVER_ERROR_BACKOFF = 20_000;
constant UNKNOWN_BACKOFF (line 35) | const UNKNOWN_BACKOFF = 60_000;
constant MIN_BACKOFF_MS (line 36) | const MIN_BACKOFF_MS = 2_000;
function generateJitter (line 42) | function generateJitter(maxJitterMs: number): number {
function parseRateLimitReason (line 46) | function parseRateLimitReason(
function calculateBackoffMs (line 96) | function calculateBackoffMs(
type BaseQuotaKey (line 125) | type BaseQuotaKey = "claude" | "gemini-antigravity" | "gemini-cli";
type QuotaKey (line 126) | type QuotaKey = BaseQuotaKey | `${BaseQuotaKey}:${string}`;
type ManagedAccount (line 128) | interface ManagedAccount {
function nowMs (line 158) | function nowMs(): number {
function clampNonNegativeInt (line 162) | function clampNonNegativeInt(value: unknown, fallback: number): number {
function getQuotaKey (line 169) | function getQuotaKey(family: ModelFamily, headerStyle: HeaderStyle, mode...
function isRateLimitedForQuotaKey (line 180) | function isRateLimitedForQuotaKey(account: ManagedAccount, key: QuotaKey...
function isRateLimitedForFamily (line 185) | function isRateLimitedForFamily(account: ManagedAccount, family: ModelFa...
function isRateLimitedForHeaderStyle (line 196) | function isRateLimitedForHeaderStyle(account: ManagedAccount, family: Mo...
function clearExpiredRateLimits (line 216) | function clearExpiredRateLimits(account: ManagedAccount): void {
function resolveQuotaGroup (line 239) | function resolveQuotaGroup(family: ModelFamily, model?: string | null): ...
function isOverSoftQuotaThreshold (line 246) | function isOverSoftQuotaThreshold(
function computeSoftQuotaCacheTtlMs (line 279) | function computeSoftQuotaCacheTtlMs(
class AccountManager (line 298) | class AccountManager {
method loadFromDisk (line 316) | static async loadFromDisk(authFallback?: OAuthAuthDetails): Promise<Ac...
method constructor (line 321) | constructor(authFallback?: OAuthAuthDetails, stored?: AccountStorageV4...
method getAccountCount (line 456) | getAccountCount(): number {
method getTotalAccountCount (line 460) | getTotalAccountCount(): number {
method getEnabledAccounts (line 464) | getEnabledAccounts(): ManagedAccount[] {
method getAccountsSnapshot (line 468) | getAccountsSnapshot(): ManagedAccount[] {
method getCurrentAccountForFamily (line 472) | getCurrentAccountForFamily(family: ModelFamily): ManagedAccount | null {
method markSwitched (line 484) | markSwitched(account: ManagedAccount, reason: "rate-limit" | "initial"...
method shouldShowAccountToast (line 493) | shouldShowAccountToast(accountIndex: number, debounceMs = 30000): bool...
method markToastShown (line 501) | markToastShown(accountIndex: number): void {
method getCurrentOrNextForFamily (line 506) | getCurrentOrNextForFamily(
method getNextForFamily (line 592) | getNextForFamily(family: ModelFamily, model?: string | null, headerSty...
method markRateLimited (line 615) | markRateLimited(
method markAccountUsed (line 631) | markAccountUsed(accountIndex: number): void {
method markRateLimitedWithReason (line 638) | markRateLimitedWithReason(
method markRequestSuccess (line 665) | markRequestSuccess(account: ManagedAccount): void {
method clearAllRateLimitsForFamily (line 671) | clearAllRateLimitsForFamily(family: ModelFamily, model?: string | null...
method shouldTryOptimisticReset (line 685) | shouldTryOptimisticReset(family: ModelFamily, model?: string | null): ...
method markAccountCoolingDown (line 690) | markAccountCoolingDown(account: ManagedAccount, cooldownMs: number, re...
method isAccountCoolingDown (line 695) | isAccountCoolingDown(account: ManagedAccount): boolean {
method clearAccountCooldown (line 706) | clearAccountCooldown(account: ManagedAccount): void {
method getAccountCooldownReason (line 711) | getAccountCooldownReason(account: ManagedAccount): CooldownReason | un...
method markTouchedForQuota (line 715) | markTouchedForQuota(account: ManagedAccount, quotaKey: string): void {
method isFreshForQuota (line 719) | isFreshForQuota(account: ManagedAccount, quotaKey: string): boolean {
method getFreshAccountsForQuota (line 729) | getFreshAccountsForQuota(quotaKey: string, family: ModelFamily, model?...
method isRateLimitedForHeaderStyle (line 739) | isRateLimitedForHeaderStyle(
method getAvailableHeaderStyle (line 748) | getAvailableHeaderStyle(account: ManagedAccount, family: ModelFamily, ...
method hasOtherAccountWithAntigravityAvailable (line 774) | hasOtherAccountWithAntigravityAvailable(
method setAccountEnabled (line 805) | setAccountEnabled(accountIndex: number, enabled: boolean): boolean {
method markAccountVerificationRequired (line 825) | markAccountVerificationRequired(accountIndex: number, reason?: string,...
method clearAccountVerificationRequired (line 849) | clearAccountVerificationRequired(accountIndex: number, enableAccount =...
method removeAccountByIndex (line 876) | removeAccountByIndex(accountIndex: number): boolean {
method removeAccount (line 887) | removeAccount(account: ManagedAccount): boolean {
method updateFromAuth (line 922) | updateFromAuth(account: ManagedAccount, auth: OAuthAuthDetails): void {
method toAuthDetails (line 934) | toAuthDetails(account: ManagedAccount): OAuthAuthDetails {
method getMinWaitTimeForFamily (line 943) | getMinWaitTimeForFamily(
method getAccounts (line 987) | getAccounts(): ManagedAccount[] {
method saveToDisk (line 991) | async saveToDisk(): Promise<void> {
method requestSaveToDisk (line 1028) | requestSaveToDisk(): void {
method flushSaveToDisk (line 1038) | async flushSaveToDisk(): Promise<void> {
method executeSave (line 1047) | private async executeSave(): Promise<void> {
method regenerateAccountFingerprint (line 1071) | regenerateAccountFingerprint(accountIndex: number): Fingerprint | null {
method restoreAccountFingerprint (line 1109) | restoreAccountFingerprint(accountIndex: number, historyIndex: number):...
method getAccountFingerprintHistory (line 1150) | getAccountFingerprintHistory(accountIndex: number): FingerprintVersion...
method updateQuotaCache (line 1158) | updateQuotaCache(accountIndex: number, quotaGroups: Partial<Record<Quo...
method isAccountOverSoftQuota (line 1166) | isAccountOverSoftQuota(account: ManagedAccount, family: ModelFamily, t...
method getAccountsForQuotaCheck (line 1170) | getAccountsForQuotaCheck(): AccountMetadataV3[] {
method getOldestQuotaCacheAge (line 1182) | getOldestQuotaCacheAge(): number | null {
method areAllAccountsOverSoftQuota (line 1193) | areAllAccountsOverSoftQuota(family: ModelFamily, thresholdPercent: num...
method getMinWaitTimeForSoftQuota (line 1206) | getMinWaitTimeForSoftQuota(
FILE: src/plugin/auth.ts
constant ACCESS_TOKEN_EXPIRY_BUFFER_MS (line 3) | const ACCESS_TOKEN_EXPIRY_BUFFER_MS = 60 * 1000;
function isOAuthAuth (line 5) | function isOAuthAuth(auth: AuthDetails): auth is OAuthAuthDetails {
function parseRefreshParts (line 12) | function parseRefreshParts(refresh: string): RefreshParts {
function formatRefreshParts (line 24) | function formatRefreshParts(parts: RefreshParts): string {
function accessTokenExpired (line 33) | function accessTokenExpired(auth: OAuthAuthDetails): boolean {
function calculateTokenExpiry (line 45) | function calculateTokenExpiry(requestTimeMs: number, expiresInSeconds: u...
FILE: src/plugin/cache.test.ts
function createAuth (line 13) | function createAuth(overrides: Partial<OAuthAuthDetails> = {}): OAuthAut...
FILE: src/plugin/cache.ts
function normalizeRefreshKey (line 10) | function normalizeRefreshKey(refresh?: string): string | undefined {
function resolveCachedAuth (line 18) | function resolveCachedAuth(auth: OAuthAuthDetails): OAuthAuthDetails {
function storeCachedAuth (line 46) | function storeCachedAuth(auth: OAuthAuthDetails): void {
function clearCachedAuth (line 57) | function clearCachedAuth(refresh?: string): void {
type SignatureEntry (line 75) | interface SignatureEntry {
constant SIGNATURE_CACHE_TTL_MS (line 84) | const SIGNATURE_CACHE_TTL_MS = 60 * 60 * 1000;
constant MAX_ENTRIES_PER_SESSION (line 87) | const MAX_ENTRIES_PER_SESSION = 100;
constant SIGNATURE_TEXT_HASH_HEX_LEN (line 90) | const SIGNATURE_TEXT_HASH_HEX_LEN = 16;
function initDiskSignatureCache (line 99) | function initDiskSignatureCache(config: SignatureCacheConfig | undefined...
function getDiskSignatureCache (line 107) | function getDiskSignatureCache(): SignatureCache | null {
function hashText (line 116) | function hashText(text: string): string {
function makeDiskKey (line 123) | function makeDiskKey(sessionId: string, textHash: string): string {
function cacheSignature (line 132) | function cacheSignature(sessionId: string, text: string, signature: stri...
function getCachedSignature (line 177) | function getCachedSignature(sessionId: string, text: string): string | u...
function clearSignatureCache (line 219) | function clearSignatureCache(sessionId?: string): void {
FILE: src/plugin/cache/signature-cache.ts
type CacheEntry (line 24) | interface CacheEntry {
type CacheData (line 35) | interface CacheData {
type CacheStats (line 49) | interface CacheStats {
type ThinkingCacheData (line 62) | interface ThinkingCacheData {
function getConfigDir (line 72) | function getConfigDir(): string {
function getCacheFilePath (line 81) | function getCacheFilePath(): string {
class SignatureCache (line 89) | class SignatureCache {
method constructor (line 113) | constructor(config: SignatureCacheConfig) {
method makeKey (line 133) | static makeKey(sessionId: string, modelId: string): string {
method store (line 140) | store(key: string, signature: string): void {
method retrieve (line 154) | retrieve(key: string): string | null {
method has (line 175) | has(key: string): boolean {
method storeThinking (line 195) | storeThinking(
method retrieveThinking (line 217) | retrieveThinking(key: string): ThinkingCacheData | null {
method hasThinking (line 240) | hasThinking(key: string): boolean {
method getStats (line 253) | getStats(): CacheStats {
method flush (line 265) | async flush(): Promise<boolean> {
method shutdown (line 273) | shutdown(): void {
method loadFromDisk (line 295) | private loadFromDisk(): void {
method saveToDisk (line 336) | private saveToDisk(): boolean {
method startBackgroundTasks (line 426) | private startBackgroundTasks(): void {
method cleanupExpired (line 443) | private cleanupExpired(): void {
function createSignatureCache (line 467) | function createSignatureCache(config: SignatureCacheConfig | undefined):...
FILE: src/plugin/cli.ts
function promptProjectId (line 12) | async function promptProjectId(): Promise<string> {
function promptAddAnotherAccount (line 22) | async function promptAddAnotherAccount(currentCount: number): Promise<bo...
type LoginMode (line 33) | type LoginMode = "add" | "fresh" | "manage" | "check" | "verify" | "veri...
type ExistingAccountInfo (line 35) | interface ExistingAccountInfo {
type LoginMenuResult (line 45) | interface LoginMenuResult {
function promptLoginModeFallback (line 55) | async function promptLoginModeFallback(existingAccounts: ExistingAccount...
function promptLoginMode (line 92) | async function promptLoginMode(existingAccounts: ExistingAccountInfo[]):...
FILE: src/plugin/config/loader.ts
function getConfigDir (line 28) | function getConfigDir(): string {
function getUserConfigPath (line 42) | function getUserConfigPath(): string {
function getProjectConfigPath (line 49) | function getProjectConfigPath(directory: string): string {
function loadConfigFile (line 60) | function loadConfigFile(path: string): Partial<AntigravityConfig> | null {
function mergeConfigs (line 94) | function mergeConfigs(
function loadConfig (line 121) | function loadConfig(directory: string): AntigravityConfig {
function configExists (line 145) | function configExists(path: string): boolean {
function getDefaultLogsDir (line 152) | function getDefaultLogsDir(): string {
function initRuntimeConfig (line 158) | function initRuntimeConfig(config: AntigravityConfig): void {
function getKeepThinking (line 162) | function getKeepThinking(): boolean {
FILE: src/plugin/config/models.ts
type ModelThinkingLevel (line 3) | type ModelThinkingLevel = "minimal" | "low" | "medium" | "high";
type ModelThinkingConfig (line 5) | interface ModelThinkingConfig {
type ModelVariant (line 9) | interface ModelVariant {
type ModelLimit (line 14) | interface ModelLimit {
type ModelModality (line 19) | type ModelModality = "text" | "image" | "pdf";
type ModelModalities (line 21) | interface ModelModalities {
type OpencodeModelDefinition (line 26) | interface OpencodeModelDefinition extends ProviderModel {
type OpencodeModelDefinitions (line 33) | type OpencodeModelDefinitions = Record<string, OpencodeModelDefinition>;
constant DEFAULT_MODALITIES (line 35) | const DEFAULT_MODALITIES: ModelModalities = {
constant OPENCODE_MODEL_DEFINITIONS (line 40) | const OPENCODE_MODEL_DEFINITIONS: OpencodeModelDefinitions = {
FILE: src/plugin/config/schema.ts
type AccountSelectionStrategy (line 22) | type AccountSelectionStrategy = z.infer<typeof AccountSelectionStrategyS...
type ToastScope (line 32) | type ToastScope = z.infer<typeof ToastScopeSchema>;
type SchedulingMode (line 42) | type SchedulingMode = z.infer<typeof SchedulingModeSchema>;
type AntigravityConfig (line 443) | type AntigravityConfig = z.infer<typeof AntigravityConfigSchema>;
type SignatureCacheConfig (line 444) | type SignatureCacheConfig = z.infer<typeof SignatureCacheConfigSchema>;
constant DEFAULT_CONFIG (line 449) | const DEFAULT_CONFIG: AntigravityConfig = {
FILE: src/plugin/config/updater.ts
type UpdateConfigResult (line 16) | interface UpdateConfigResult {
type OpencodeConfig (line 22) | interface OpencodeConfig {
type UpdateConfigOptions (line 35) | interface UpdateConfigOptions {
constant PLUGIN_NAME (line 44) | const PLUGIN_NAME = "opencode-antigravity-auth@latest";
constant SCHEMA_URL (line 45) | const SCHEMA_URL = "https://opencode.ai/config.json";
constant OPENCODE_JSON_FILENAME (line 46) | const OPENCODE_JSON_FILENAME = "opencode.json";
constant OPENCODE_JSONC_FILENAME (line 47) | const OPENCODE_JSONC_FILENAME = "opencode.jsonc";
function stripJsonCommentsAndTrailingCommas (line 49) | function stripJsonCommentsAndTrailingCommas(json: string): string {
function getOpencodeConfigDir (line 61) | function getOpencodeConfigDir(): string {
function getOpencodeConfigPath (line 72) | function getOpencodeConfigPath(): string {
function updateOpencodeConfig (line 107) | async function updateOpencodeConfig(
FILE: src/plugin/core/streaming/transformer.ts
function hashString (line 13) | function hashString(str: string): string {
function createThoughtBuffer (line 21) | function createThoughtBuffer(): ThoughtBuffer {
function transformStreamingPayload (line 30) | function transformStreamingPayload(
function deduplicateThinkingText (line 58) | function deduplicateThinkingText(
function transformSseLine (line 175) | function transformSseLine(
function cacheThinkingSignaturesFromResponse (line 226) | function cacheThinkingSignaturesFromResponse(
function createStreamingTransformer (line 291) | function createStreamingTransformer(
FILE: src/plugin/core/streaming/types.ts
type SignedThinking (line 1) | interface SignedThinking {
type SignatureStore (line 6) | interface SignatureStore {
type StreamingCallbacks (line 13) | interface StreamingCallbacks {
type StreamingOptions (line 20) | interface StreamingOptions {
type ThoughtBuffer (line 28) | interface ThoughtBuffer {
FILE: src/plugin/debug.ts
constant MAX_BODY_PREVIEW_CHARS (line 17) | const MAX_BODY_PREVIEW_CHARS = 12000;
constant MAX_BODY_LOG_CHARS (line 18) | const MAX_BODY_LOG_CHARS = 50000;
constant DEBUG_MESSAGE_PREFIX (line 20) | const DEBUG_MESSAGE_PREFIX = "[opencode-antigravity-auth debug]";
type DebugState (line 26) | interface DebugState {
function getConfigDir (line 38) | function getConfigDir(): string {
function getLogsDir (line 50) | function getLogsDir(customLogDir?: string): string {
function createLogFilePath (line 65) | function createLogFilePath(customLogDir?: string): string {
function cleanupOldLogs (line 75) | function cleanupOldLogs(logsDir: string, maxFiles: number): void {
function createLogWriter (line 107) | function createLogWriter(filePath?: string): (line: string) => void {
function initializeDebug (line 129) | function initializeDebug(config: AntigravityConfig): void {
function getDebugState (line 158) | function getDebugState(): DebugState {
function isDebugEnabled (line 185) | function isDebugEnabled(): boolean {
function isDebugTuiEnabled (line 189) | function isDebugTuiEnabled(): boolean {
function getLogFilePath (line 193) | function getLogFilePath(): string | undefined {
type AntigravityDebugContext (line 197) | interface AntigravityDebugContext {
type AntigravityDebugRequestMeta (line 203) | interface AntigravityDebugRequestMeta {
type AntigravityDebugResponseMeta (line 213) | interface AntigravityDebugResponseMeta {
function startAntigravityDebugRequest (line 225) | function startAntigravityDebugRequest(meta: AntigravityDebugRequestMeta)...
function logAntigravityDebugResponse (line 253) | function logAntigravityDebugResponse(
function maskHeaders (line 291) | function maskHeaders(headers?: HeadersInit | Headers): Record<string, st...
function logDebug (line 311) | function logDebug(line: string): void {
function runWithDebugEnabled (line 315) | function runWithDebugEnabled(action: () => void): void {
type AccountDebugInfo (line 320) | interface AccountDebugInfo {
function logAccountContext (line 328) | function logAccountContext(label: string, info: AccountDebugInfo): void {
function logRateLimitEvent (line 353) | function logRateLimitEvent(
function logRateLimitSnapshot (line 379) | function logRateLimitSnapshot(
function logResponseBody (line 399) | async function logResponseBody(
function logModelFamily (line 418) | function logModelFamily(url: string, extractedModel: string | null, fami...
function debugLogToFile (line 424) | function debugLogToFile(message: string): void {
function logToast (line 434) | function logToast(message: string, variant: "info" | "warning" | "succes...
function logRetryAttempt (line 445) | function logRetryAttempt(
function logCacheStats (line 461) | function logCacheStats(
function logQuotaStatus (line 479) | function logQuotaStatus(
function logQuotaFetch (line 496) | function logQuotaFetch(
function logModelUsed (line 511) | function logModelUsed(
FILE: src/plugin/errors.ts
class EmptyResponseError (line 15) | class EmptyResponseError extends Error {
method constructor (line 20) | constructor(
class ToolIdMismatchError (line 41) | class ToolIdMismatchError extends Error {
method constructor (line 45) | constructor(expectedIds: string[], foundIds: string[], message?: strin...
FILE: src/plugin/fingerprint.ts
constant OS_VERSIONS (line 15) | const OS_VERSIONS: Record<string, string[]> = {
constant ARCHITECTURES (line 21) | const ARCHITECTURES = ["x64", "arm64"];
constant IDE_TYPES (line 23) | const IDE_TYPES = [
constant PLATFORMS (line 27) | const PLATFORMS = [
constant SDK_CLIENTS (line 32) | const SDK_CLIENTS = [
type ClientMetadata (line 39) | interface ClientMetadata {
type Fingerprint (line 45) | interface Fingerprint {
type FingerprintVersion (line 60) | interface FingerprintVersion {
constant MAX_FINGERPRINT_HISTORY (line 67) | const MAX_FINGERPRINT_HISTORY = 5;
type FingerprintHeaders (line 69) | interface FingerprintHeaders {
function randomFrom (line 73) | function randomFrom<T>(arr: readonly T[]): T {
function generateDeviceId (line 77) | function generateDeviceId(): string {
function generateSessionToken (line 81) | function generateSessionToken(): string {
function generateFingerprint (line 89) | function generateFingerprint(): Fingerprint {
function collectCurrentFingerprint (line 117) | function collectCurrentFingerprint(): Fingerprint {
function updateFingerprintVersion (line 145) | function updateFingerprintVersion(fingerprint: Fingerprint): boolean {
function buildFingerprintHeaders (line 162) | function buildFingerprintHeaders(fingerprint: Fingerprint | null): Parti...
function getSessionFingerprint (line 182) | function getSessionFingerprint(): Fingerprint {
function regenerateSessionFingerprint (line 193) | function regenerateSessionFingerprint(): Fingerprint {
FILE: src/plugin/image-saver.ts
function getImageOutputDir (line 15) | function getImageOutputDir(): string {
function generateImageFilename (line 30) | function generateImageFilename(mimeType: string): string {
function saveImageToDisk (line 54) | function saveImageToDisk(base64Data: string, mimeType: string): string {
function processImageData (line 79) | function processImageData(inlineData: { mimeType?: string; data?: string...
FILE: src/plugin/logger.ts
type LogLevel (line 18) | type LogLevel = "debug" | "info" | "warn" | "error";
constant ENV_CONSOLE_LOG (line 20) | const ENV_CONSOLE_LOG = "OPENCODE_ANTIGRAVITY_CONSOLE_LOG";
type Logger (line 22) | interface Logger {
function isConsoleLogEnabled (line 34) | function isConsoleLogEnabled(): boolean {
function initLogger (line 42) | function initLogger(client: PluginClient): void {
function createLogger (line 59) | function createLogger(module: string): Logger {
FILE: src/plugin/logging-utils.ts
type LogLevel (line 1) | type LogLevel = "debug" | "info" | "warn" | "error"
type DebugPolicyInput (line 3) | interface DebugPolicyInput {
type DebugPolicy (line 10) | interface DebugPolicy {
function isTruthyFlag (line 17) | function isTruthyFlag(flag?: string): boolean {
function parseDebugLevel (line 21) | function parseDebugLevel(flag: string): number {
function deriveDebugPolicy (line 28) | function deriveDebugPolicy(input: DebugPolicyInput): DebugPolicy {
function formatAccountLabel (line 47) | function formatAccountLabel(email: string | undefined, accountIndex: num...
function formatAccountContextLabel (line 51) | function formatAccountContextLabel(email: string | undefined, accountInd...
function formatErrorForLog (line 61) | function formatErrorForLog(error: unknown): string {
function truncateTextForLog (line 72) | function truncateTextForLog(text: string, maxChars: number): string {
function formatBodyPreviewForLog (line 79) | function formatBodyPreviewForLog(
function writeConsoleLog (line 106) | function writeConsoleLog(level: LogLevel, ...args: unknown[]): void {
FILE: src/plugin/persist-account-pool.test.ts
function createMockAccount (line 37) | function createMockAccount(overrides: Partial<AccountMetadataV3> = {}): ...
function createMockStorage (line 49) | function createMockStorage(accounts: AccountMetadataV3[], activeIndex = ...
FILE: src/plugin/project.ts
constant CODE_ASSIST_METADATA (line 16) | const CODE_ASSIST_METADATA = {
type AntigravityUserTier (line 22) | interface AntigravityUserTier {
type LoadCodeAssistPayload (line 28) | interface LoadCodeAssistPayload {
type OnboardUserPayload (line 36) | interface OnboardUserPayload {
function buildMetadata (line 45) | function buildMetadata(projectId?: string): Record<string, string> {
function getDefaultTierId (line 60) | function getDefaultTierId(allowedTiers?: AntigravityUserTier[]): string ...
function wait (line 75) | function wait(ms: number): Promise<void> {
function extractManagedProjectId (line 84) | function extractManagedProjectId(payload: LoadCodeAssistPayload | null):...
function getCacheKey (line 100) | function getCacheKey(auth: OAuthAuthDetails): string | undefined {
function invalidateProjectContextCache (line 108) | function invalidateProjectContextCache(refresh?: string): void {
function loadManagedProject (line 121) | async function loadManagedProject(
function onboardManagedProject (line 169) | async function onboardManagedProject(
function ensureProjectContext (line 225) | async function ensureProjectContext(auth: OAuthAuthDetails): Promise<Pro...
FILE: src/plugin/quota-fallback.test.ts
type ResolveQuotaFallbackHeaderStyle (line 4) | type ResolveQuotaFallbackHeaderStyle = (input: {
type GetHeaderStyleFromUrl (line 10) | type GetHeaderStyleFromUrl = (
type ResolveHeaderRoutingDecision (line 16) | type ResolveHeaderRoutingDecision = (
FILE: src/plugin/quota.ts
constant FETCH_TIMEOUT_MS (line 14) | const FETCH_TIMEOUT_MS = 10000;
type QuotaGroup (line 16) | type QuotaGroup = "claude" | "gemini-pro" | "gemini-flash";
type QuotaGroupSummary (line 18) | interface QuotaGroupSummary {
type QuotaSummary (line 24) | interface QuotaSummary {
type GeminiCliQuotaModel (line 31) | interface GeminiCliQuotaModel {
type GeminiCliQuotaSummary (line 37) | interface GeminiCliQuotaSummary {
type RetrieveUserQuotaResponse (line 42) | interface RetrieveUserQuotaResponse {
type AccountQuotaStatus (line 52) | type AccountQuotaStatus = "ok" | "disabled" | "error";
type AccountQuotaResult (line 54) | interface AccountQuotaResult {
type FetchAvailableModelsResponse (line 65) | interface FetchAvailableModelsResponse {
type FetchAvailableModelEntry (line 69) | interface FetchAvailableModelEntry {
function buildAuthFromAccount (line 78) | function buildAuthFromAccount(account: AccountMetadataV3): OAuthAuthDeta...
function normalizeRemainingFraction (line 91) | function normalizeRemainingFraction(value: unknown): number {
function parseResetTime (line 101) | function parseResetTime(resetTime?: string): number | null {
function classifyQuotaGroup (line 110) | function classifyQuotaGroup(modelName: string, displayName?: string): Qu...
function aggregateQuota (line 123) | function aggregateQuota(models?: Record<string, FetchAvailableModelEntry...
function fetchWithTimeout (line 175) | async function fetchWithTimeout(url: string, options: RequestInit, timeo...
function fetchAvailableModels (line 185) | async function fetchAvailableModels(
function fetchGeminiCliQuota (line 217) | async function fetchGeminiCliQuota(
function aggregateGeminiCliQuota (line 253) | function aggregateGeminiCliQuota(response: RetrieveUserQuotaResponse): G...
function applyAccountUpdates (line 289) | function applyAccountUpdates(account: AccountMetadataV3, auth: OAuthAuth...
function checkAccountsQuota (line 310) | async function checkAccountsQuota(
FILE: src/plugin/recovery.ts
constant RECOVERY_RESUME_TEXT (line 36) | const RECOVERY_RESUME_TEXT = "[session recovered - continuing previous t...
function getErrorMessage (line 45) | function getErrorMessage(error: unknown): string {
function extractMessageIndex (line 76) | function extractMessageIndex(error: unknown): number | null {
function detectErrorType (line 86) | function detectErrorType(error: unknown): RecoveryErrorType {
function isRecoverableError (line 120) | function isRecoverableError(error: unknown): boolean {
type ToolUsePart (line 128) | interface ToolUsePart {
function extractToolUseIds (line 135) | function extractToolUseIds(parts: MessagePart[]): string[] {
function recoverToolResultMissing (line 148) | async function recoverToolResultMissing(
function recoverThinkingBlockOrder (line 193) | async function recoverThinkingBlockOrder(
function recoverThinkingDisabledViolation (line 227) | async function recoverThinkingDisabledViolation(
function findLastUserMessage (line 251) | function findLastUserMessage(messages: MessageData[]): MessageData | und...
function extractResumeConfig (line 260) | function extractResumeConfig(userMessage: MessageData | undefined, sessi...
function resumeSession (line 268) | async function resumeSession(
constant TOAST_TITLES (line 293) | const TOAST_TITLES: Record<string, string> = {
constant TOAST_MESSAGES (line 299) | const TOAST_MESSAGES: Record<string, string> = {
function getRecoveryToastContent (line 305) | function getRecoveryToastContent(errorType: RecoveryErrorType): {
function getRecoverySuccessToast (line 321) | function getRecoverySuccessToast(): {
function getRecoveryFailureToast (line 331) | function getRecoveryFailureToast(): {
type SessionRecoveryHook (line 345) | interface SessionRecoveryHook {
type SessionRecoveryContext (line 368) | interface SessionRecoveryContext {
function createSessionRecoveryHook (line 376) | function createSessionRecoveryHook(
FILE: src/plugin/recovery/constants.ts
function getXdgData (line 14) | function getXdgData(): string {
function getXdgConfig (line 28) | function getXdgConfig(): string {
function getAntigravityConfigDir (line 42) | function getAntigravityConfigDir(): string {
constant OPENCODE_STORAGE (line 46) | const OPENCODE_STORAGE = join(getXdgData(), "opencode", "storage");
constant MESSAGE_STORAGE (line 47) | const MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message");
constant PART_STORAGE (line 48) | const PART_STORAGE = join(OPENCODE_STORAGE, "part");
constant THINKING_TYPES (line 50) | const THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reason...
constant META_TYPES (line 51) | const META_TYPES = new Set(["step-start", "step-finish"]);
constant CONTENT_TYPES (line 52) | const CONTENT_TYPES = new Set(["text", "tool", "tool_use", "tool_result"]);
FILE: src/plugin/recovery/storage.ts
function generatePartId (line 16) | function generatePartId(): string {
function getMessageDir (line 26) | function getMessageDir(sessionID: string): string {
function readMessages (line 53) | function readMessages(sessionID: string): StoredMessageMeta[] {
function readParts (line 84) | function readParts(messageID: string): StoredPart[] {
function hasContent (line 110) | function hasContent(part: StoredPart): boolean {
function messageHasContent (line 130) | function messageHasContent(messageID: string): boolean {
function injectTextPart (line 139) | function injectTextPart(sessionID: string, messageID: string, text: stri...
function findMessagesWithThinkingBlocks (line 168) | function findMessagesWithThinkingBlocks(sessionID: string): string[] {
function findMessagesWithThinkingOnly (line 185) | function findMessagesWithThinkingOnly(sessionID: string): string[] {
function findMessagesWithOrphanThinking (line 207) | function findMessagesWithOrphanThinking(sessionID: string): string[] {
function prependThinkingPart (line 233) | function prependThinkingPart(sessionID: string, messageID: string): bool...
function stripThinkingParts (line 258) | function stripThinkingParts(messageID: string): boolean {
function findEmptyMessages (line 289) | function findEmptyMessages(sessionID: string): string[] {
function findEmptyMessageByIndex (line 302) | function findEmptyMessageByIndex(sessionID: string, targetIndex: number)...
function findMessageByIndexNeedingThinking (line 322) | function findMessageByIndexNeedingThinking(sessionID: string, targetInde...
function replaceEmptyTextParts (line 346) | function replaceEmptyTextParts(messageID: string, replacementText: strin...
function findMessagesWithEmptyTextParts (line 379) | function findMessagesWithEmptyTextParts(sessionID: string): string[] {
FILE: src/plugin/recovery/types.ts
type ThinkingPartType (line 11) | type ThinkingPartType = "thinking" | "redacted_thinking" | "reasoning";
type MetaPartType (line 12) | type MetaPartType = "step-start" | "step-finish";
type ContentPartType (line 13) | type ContentPartType = "text" | "tool" | "tool_use" | "tool_result";
type StoredMessageMeta (line 15) | interface StoredMessageMeta {
type StoredTextPart (line 27) | interface StoredTextPart {
type StoredToolPart (line 37) | interface StoredToolPart {
type StoredReasoningPart (line 52) | interface StoredReasoningPart {
type StoredStepPart (line 60) | interface StoredStepPart {
type StoredPart (line 67) | type StoredPart =
type MessagePart (line 84) | interface MessagePart {
type MessageData (line 94) | interface MessageData {
type MessageInfo (line 112) | interface MessageInfo {
type ResumeConfig (line 120) | interface ResumeConfig {
type RecoveryErrorType (line 133) | type RecoveryErrorType =
type ToolUsePart (line 139) | interface ToolUsePart {
type ToolResultPart (line 146) | interface ToolResultPart {
FILE: src/plugin/refresh-queue.ts
type ProactiveRefreshConfig (line 27) | interface ProactiveRefreshConfig {
constant DEFAULT_PROACTIVE_REFRESH_CONFIG (line 36) | const DEFAULT_PROACTIVE_REFRESH_CONFIG: ProactiveRefreshConfig = {
type RefreshQueueState (line 43) | interface RefreshQueueState {
class ProactiveRefreshQueue (line 61) | class ProactiveRefreshQueue {
method constructor (line 77) | constructor(
method setAccountManager (line 94) | setAccountManager(manager: AccountManager): void {
method needsRefresh (line 102) | needsRefresh(account: ManagedAccount): boolean {
method isExpired (line 118) | isExpired(account: ManagedAccount): boolean {
method getAccountsNeedingRefresh (line 128) | getAccountsNeedingRefresh(): ManagedAccount[] {
method runRefreshCheck (line 150) | private async runRefreshCheck(): Promise<void> {
method refreshToken (line 212) | private async refreshToken(
method start (line 232) | start(): void {
method stop (line 274) | stop(): void {
method getStats (line 295) | getStats(): {
method isRunning (line 309) | isRunning(): boolean {
function createProactiveRefreshQueue (line 317) | function createProactiveRefreshQueue(
FILE: src/plugin/request-helpers.test.ts
function createTestBuffer (line 1759) | function createTestBuffer() {
FILE: src/plugin/request-helpers.ts
constant ANTIGRAVITY_PREVIEW_LINK (line 14) | const ANTIGRAVITY_PREVIEW_LINK = "https://goo.gle/enable-preview-features";
constant UNSUPPORTED_CONSTRAINTS (line 25) | const UNSUPPORTED_CONSTRAINTS = [
constant UNSUPPORTED_KEYWORDS (line 34) | const UNSUPPORTED_KEYWORDS = [
function appendDescriptionHint (line 43) | function appendDescriptionHint(schema: any, hint: string): any {
function convertRefsToHints (line 56) | function convertRefsToHints(schema: any): any {
function convertConstToEnum (line 87) | function convertConstToEnum(schema: any): any {
function addEnumHints (line 111) | function addEnumHints(schema: any): any {
function addAdditionalPropertiesHints (line 142) | function addAdditionalPropertiesHints(schema: any): any {
function moveConstraintsToDescription (line 171) | function moveConstraintsToDescription(schema: any): any {
function mergeAllOf (line 204) | function mergeAllOf(schema: any): any {
function scoreSchemaOption (line 278) | function scoreSchemaOption(schema: any): { score: number; typeName: stri...
function tryMergeEnumFromUnion (line 313) | function tryMergeEnumFromUnion(options: any[]): string[] | null {
function flattenAnyOfOneOf (line 369) | function flattenAnyOfOneOf(schema: any): any {
function flattenTypeArrays (line 460) | function flattenTypeArrays(schema: any, nullableFields?: Map<string, str...
function removeUnsupportedKeywords (line 540) | function removeUnsupportedKeywords(schema: any, insideProperties: boolea...
function cleanupRequiredFields (line 575) | function cleanupRequiredFields(schema: any): any {
function addEmptySchemaPlaceholder (line 612) | function addEmptySchemaPlaceholder(schema: any): any {
function cleanJSONSchemaForAntigravity (line 659) | function cleanJSONSchemaForAntigravity(schema: any): any {
type AntigravityApiError (line 692) | interface AntigravityApiError {
type AntigravityApiBody (line 702) | interface AntigravityApiBody {
type AntigravityUsageMetadata (line 711) | interface AntigravityUsageMetadata {
type ThinkingConfig (line 722) | interface ThinkingConfig {
constant DEFAULT_THINKING_BUDGET (line 731) | const DEFAULT_THINKING_BUDGET = 16000;
function isThinkingCapableModel (line 737) | function isThinkingCapableModel(modelName: string): boolean {
function extractThinkingConfig (line 748) | function extractThinkingConfig(
type VariantThinkingConfig (line 783) | interface VariantThinkingConfig {
function extractVariantThinkingConfig (line 807) | function extractVariantThinkingConfig(
function resolveThinkingConfig (line 863) | function resolveThinkingConfig(
function isThinkingPart (line 881) | function isThinkingPart(part: Record<string, unknown>): boolean {
function hasSignatureField (line 893) | function hasSignatureField(part: Record<string, unknown>): boolean {
function isToolBlock (line 905) | function isToolBlock(part: Record<string, unknown>): boolean {
function stripAllThinkingBlocks (line 922) | function stripAllThinkingBlocks(contentArray: any[]): any[] {
function removeTrailingThinkingBlocks (line 937) | function removeTrailingThinkingBlocks(
function hasValidSignature (line 962) | function hasValidSignature(part: Record<string, unknown>): boolean {
function getSignature (line 970) | function getSignature(part: Record<string, unknown>): string | undefined {
function isOurCachedSignature (line 980) | function isOurCachedSignature(
function getThinkingText (line 1006) | function getThinkingText(part: Record<string, unknown>): string {
function stripCacheControlRecursively (line 1027) | function stripCacheControlRecursively(obj: unknown): unknown {
function sanitizeThinkingPart (line 1045) | function sanitizeThinkingPart(part: Record<string, unknown>): Record<str...
function findLastAssistantIndex (line 1107) | function findLastAssistantIndex(contents: any[], roleValue: "model" | "a...
function filterContentArray (line 1117) | function filterContentArray(
function filterUnsignedThinkingBlocks (line 1235) | function filterUnsignedThinkingBlocks(
function filterMessagesThinkingBlocks (line 1293) | function filterMessagesThinkingBlocks(
function deepFilterThinkingBlocks (line 1329) | function deepFilterThinkingBlocks(
function transformGeminiCandidate (line 1385) | function transformGeminiCandidate(candidate: any): any {
function transformThinkingParts (line 1489) | function transformThinkingParts(response: unknown): unknown {
function normalizeThinkingConfig (line 1546) | function normalizeThinkingConfig(config: unknown): ThinkingConfig | unde...
function parseAntigravityApiBody (line 1578) | function parseAntigravityApiBody(rawText: string): AntigravityApiBody | ...
function extractUsageMetadata (line 1602) | function extractUsageMetadata(body: AntigravityApiBody): AntigravityUsag...
function extractUsageFromSsePayload (line 1627) | function extractUsageFromSsePayload(payload: string): AntigravityUsageMe...
function rewriteAntigravityPreviewAccessError (line 1655) | function rewriteAntigravityPreviewAccessError(
function needsPreviewAccessOverride (line 1680) | function needsPreviewAccessOverride(
function isAntigravityModel (line 1697) | function isAntigravityModel(target?: string): boolean {
function isEmptyResponseBody (line 1721) | function isEmptyResponseBody(text: string): boolean {
type StreamingChunkCounter (line 1810) | interface StreamingChunkCounter {
function createStreamingChunkCounter (line 1816) | function createStreamingChunkCounter(): StreamingChunkCounter {
function isMeaningfulSseLine (line 1835) | function isMeaningfulSseLine(line: string): boolean {
constant SKIP_PARSE_KEYS (line 1900) | const SKIP_PARSE_KEYS = new Set([
function recursivelyParseJsonStrings (line 1926) | function recursivelyParseJsonStrings(
function fixToolResponseGrouping (line 2044) | function fixToolResponseGrouping(contents: any[]): any[] {
function detectToolIdMismatches (line 2224) | function detectToolIdMismatches(contents: any[]): {
function findOrphanedToolUseIds (line 2270) | function findOrphanedToolUseIds(messages: any[]): Set<string> {
function fixClaudeToolPairing (line 2301) | function fixClaudeToolPairing(messages: any[]): any[] {
function removeOrphanedToolUse (line 2396) | function removeOrphanedToolUse(messages: any[], orphanIds: Set<string>):...
function validateAndFixClaudeToolPairing (line 2420) | function validateAndFixClaudeToolPairing(messages: any[]): any[] {
function formatTypeHint (line 2452) | function formatTypeHint(propData: Record<string, unknown>, depth = 0): s...
function injectParameterSignatures (line 2519) | function injectParameterSignatures(
function injectToolHardeningInstruction (line 2568) | function injectToolHardeningInstruction(
function assignToolIdsToContents (line 2622) | function assignToolIdsToContents(
function matchResponseIdsToContents (line 2666) | function matchResponseIdsToContents(
function applyToolPairingFixes (line 2710) | function applyToolPairingFixes(
function createSyntheticErrorResponse (line 2771) | function createSyntheticErrorResponse(
FILE: src/plugin/request.test.ts
function createMockSignatureStore (line 37) | function createMockSignatureStore(): SignatureStore {
function createMockThoughtBuffer (line 47) | function createMockThoughtBuffer(): ThoughtBuffer {
function withKeepThinking (line 60) | function withKeepThinking<T>(enabled: boolean, fn: () => T): T {
FILE: src/plugin/request.ts
constant PLUGIN_SESSION_ID (line 77) | const PLUGIN_SESSION_ID = `-${crypto.randomUUID()}`;
constant MIN_SIGNATURE_LENGTH (line 81) | const MIN_SIGNATURE_LENGTH = 50;
function buildSignatureSessionKey (line 83) | function buildSignatureSessionKey(
function shouldCacheThinkingSignatures (line 105) | function shouldCacheThinkingSignatures(model?: string): boolean {
function hashConversationSeed (line 113) | function hashConversationSeed(seed: string): string {
function extractTextFromContent (line 117) | function extractTextFromContent(content: unknown): string {
function extractConversationSeedFromMessages (line 139) | function extractConversationSeedFromMessages(messages: any[]): string {
function extractConversationSeedFromContents (line 150) | function extractConversationSeedFromContents(contents: any[]): string {
function resolveConversationKey (line 164) | function resolveConversationKey(requestPayload: Record<string, unknown>)...
function resolveConversationKeyFromRequests (line 205) | function resolveConversationKeyFromRequests(requestObjects: Array<Record...
function resolveProjectKey (line 215) | function resolveProjectKey(candidate?: unknown, fallback?: string): stri...
function formatDebugLinesForThinking (line 225) | function formatDebugLinesForThinking(lines: string[]): string {
function injectDebugThinking (line 234) | function injectDebugThinking(response: unknown, debugText: string): unkn...
constant SYNTHETIC_THINKING_PLACEHOLDER (line 277) | const SYNTHETIC_THINKING_PLACEHOLDER = "[Thinking preserved]\n";
function stripInjectedDebugFromParts (line 279) | function stripInjectedDebugFromParts(parts: unknown): unknown {
function stripInjectedDebugFromRequestPayload (line 306) | function stripInjectedDebugFromRequestPayload(payload: Record<string, un...
function isValidRequestPart (line 342) | function isValidRequestPart(part: unknown): boolean {
function sanitizeRequestPayloadForAntigravity (line 361) | function sanitizeRequestPayloadForAntigravity(payload: Record<string, un...
function isGeminiToolUsePart (line 431) | function isGeminiToolUsePart(part: any): boolean {
function isGeminiThinkingPart (line 435) | function isGeminiThinkingPart(part: any): boolean {
constant SENTINEL_SIGNATURE (line 446) | const SENTINEL_SIGNATURE = "skip_thought_signature_validator";
function getThinkingPartText (line 448) | function getThinkingPartText(part: any): string {
function hasCachedMatchingSignature (line 464) | function hasCachedMatchingSignature(part: any, sessionId: string): boole...
function ensureThoughtSignature (line 486) | function ensureThoughtSignature(part: any, sessionId: string): any {
function hasSignedThinkingPart (line 511) | function hasSignedThinkingPart(part: any, sessionId?: string): boolean {
function ensureThinkingBeforeToolUseInContents (line 551) | function ensureThinkingBeforeToolUseInContents(contents: any[], signatur...
function ensureMessageThinkingSignature (line 595) | function ensureMessageThinkingSignature(block: any, sessionId: string): ...
function hasToolUseInContents (line 616) | function hasToolUseInContents(contents: any[]): boolean {
function hasSignedThinkingInContents (line 625) | function hasSignedThinkingInContents(contents: any[], sessionId?: string...
function hasToolUseInMessages (line 634) | function hasToolUseInMessages(messages: any[]): boolean {
function hasSignedThinkingInMessages (line 645) | function hasSignedThinkingInMessages(messages: any[], sessionId?: string...
function ensureThinkingBeforeToolUseInMessages (line 654) | function ensureThinkingBeforeToolUseInMessages(messages: any[], signatur...
function getPluginSessionId (line 709) | function getPluginSessionId(): string {
function generateSyntheticProjectId (line 713) | function generateSyntheticProjectId(): string {
constant STREAM_ACTION (line 722) | const STREAM_ACTION = "streamGenerateContent";
function isGenerativeLanguageRequest (line 727) | function isGenerativeLanguageRequest(input: RequestInfo): input is string {
type PrepareRequestOptions (line 734) | interface PrepareRequestOptions {
function prepareAntigravityRequest (line 745) | function prepareAntigravityRequest(
function buildThinkingWarmupBody (line 1578) | function buildThinkingWarmupBody(
function transformAntigravityResponse (line 1629) | async function transformAntigravityResponse(
FILE: src/plugin/rotation.ts
type HealthScoreConfig (line 16) | interface HealthScoreConfig {
constant DEFAULT_HEALTH_SCORE_CONFIG (line 33) | const DEFAULT_HEALTH_SCORE_CONFIG: HealthScoreConfig = {
type HealthScoreState (line 43) | interface HealthScoreState {
class HealthScoreTracker (line 54) | class HealthScoreTracker {
method constructor (line 58) | constructor(config: Partial<HealthScoreConfig> = {}) {
method getScore (line 65) | getScore(accountIndex: number): number {
method recordSuccess (line 85) | recordSuccess(accountIndex: number): void {
method recordRateLimit (line 100) | recordRateLimit(accountIndex: number): void {
method recordFailure (line 116) | recordFailure(accountIndex: number): void {
method isUsable (line 132) | isUsable(accountIndex: number): boolean {
method getConsecutiveFailures (line 139) | getConsecutiveFailures(accountIndex: number): number {
method reset (line 146) | reset(accountIndex: number): void {
method getSnapshot (line 153) | getSnapshot(): Map<number, { score: number; consecutiveFailures: numbe...
function addJitter (line 177) | function addJitter(baseMs: number, jitterFactor: number = 0.3): number {
function randomDelay (line 190) | function randomDelay(minMs: number, maxMs: number): number {
type AccountWithMetrics (line 198) | interface AccountWithMetrics {
function sortByLruWithHealth (line 215) | function sortByLruWithHealth(
constant STICKINESS_BONUS (line 232) | const STICKINESS_BONUS = 150;
constant SWITCH_THRESHOLD (line 235) | const SWITCH_THRESHOLD = 100;
function selectHybridAccount (line 250) | function selectHybridAccount(
type AccountWithTokens (line 306) | interface AccountWithTokens extends AccountWithMetrics {
function calculateHybridScore (line 310) | function calculateHybridScore(
type TokenBucketConfig (line 325) | interface TokenBucketConfig {
constant DEFAULT_TOKEN_BUCKET_CONFIG (line 334) | const DEFAULT_TOKEN_BUCKET_CONFIG: TokenBucketConfig = {
type TokenBucketState (line 340) | interface TokenBucketState {
class TokenBucketTracker (line 349) | class TokenBucketTracker {
method constructor (line 353) | constructor(config: Partial<TokenBucketConfig> = {}) {
method getTokens (line 360) | getTokens(accountIndex: number): number {
method hasTokens (line 380) | hasTokens(accountIndex: number, cost: number = 1): boolean {
method consume (line 388) | consume(accountIndex: number, cost: number = 1): boolean {
method refund (line 404) | refund(accountIndex: number, amount: number = 1): void {
method getMaxTokens (line 412) | getMaxTokens(): number {
function getTokenTracker (line 423) | function getTokenTracker(): TokenBucketTracker {
function initTokenTracker (line 430) | function initTokenTracker(config: Partial<TokenBucketConfig>): TokenBuck...
function getHealthTracker (line 441) | function getHealthTracker(): HealthScoreTracker {
function initHealthTracker (line 452) | function initHealthTracker(config: Partial<HealthScoreConfig>): HealthSc...
FILE: src/plugin/search.ts
type GroundingChunk (line 24) | interface GroundingChunk {
type GroundingSupport (line 31) | interface GroundingSupport {
type GroundingMetadata (line 40) | interface GroundingMetadata {
type UrlMetadata (line 49) | interface UrlMetadata {
type UrlContextMetadata (line 54) | interface UrlContextMetadata {
type SearchResponse (line 58) | interface SearchResponse {
type AntigravitySearchResponse (line 75) | interface AntigravitySearchResponse {
type SearchArgs (line 84) | interface SearchArgs {
type SearchResult (line 90) | interface SearchResult {
function generateRequestId (line 104) | function generateRequestId(): string {
function getSessionId (line 108) | function getSessionId(): string {
function formatSearchResult (line 113) | function formatSearchResult(result: SearchResult): string {
function parseSearchResponse (line 147) | function parseSearchResponse(data: AntigravitySearchResponse): SearchRes...
function executeSearch (line 223) | async function executeSearch(
FILE: src/plugin/server.ts
type OAuthListenerOptions (line 6) | interface OAuthListenerOptions {
type OAuthListener (line 13) | interface OAuthListener {
function isOrbStackDockerHost (line 31) | function isOrbStackDockerHost(): boolean {
function isWSL (line 83) | function isWSL(): boolean {
function isRemoteEnvironment (line 96) | function isRemoteEnvironment(): boolean {
function getBindAddress (line 115) | function getBindAddress(): string {
function startOAuthListener (line 140) | async function startOAuthListener(
FILE: src/plugin/storage.ts
constant GITIGNORE_ENTRIES (line 25) | const GITIGNORE_ENTRIES = [
function ensureGitignore (line 38) | async function ensureGitignore(configDir: string): Promise<void> {
function ensureGitignoreSync (line 89) | function ensureGitignoreSync(configDir: string): void {
type ModelFamily (line 130) | type ModelFamily = "claude" | "gemini";
type RateLimitState (line 133) | interface RateLimitState {
type RateLimitStateV3 (line 138) | interface RateLimitStateV3 {
type AccountMetadataV1 (line 145) | interface AccountMetadataV1 {
type AccountStorageV1 (line 157) | interface AccountStorageV1 {
type AccountMetadata (line 163) | interface AccountMetadata {
type AccountStorage (line 174) | interface AccountStorage {
type CooldownReason (line 180) | type CooldownReason = "auth-failure" | "network-error" | "project-error"...
type AccountMetadataV3 (line 182) | interface AccountMetadataV3 {
type AccountStorageV3 (line 207) | interface AccountStorageV3 {
type AccountStorageV4 (line 217) | interface AccountStorageV4 {
type AnyAccountStorage (line 227) | type AnyAccountStorage =
function getLegacyWindowsConfigDir (line 237) | function getLegacyWindowsConfigDir(): string {
function getConfigDir (line 251) | function getConfigDir(): string {
function migrateLegacyWindowsConfig (line 267) | function migrateLegacyWindowsConfig(): boolean {
function getStoragePathWithMigration (line 315) | function getStoragePathWithMigration(): string {
function getStoragePath (line 341) | function getStoragePath(): string {
constant LOCK_OPTIONS (line 350) | const LOCK_OPTIONS = {
function ensureSecurePermissions (line 364) | async function ensureSecurePermissions(path: string): Promise<void> {
function ensureFileExists (line 372) | async function ensureFileExists(path: string): Promise<void> {
function withFileLock (line 385) | async function withFileLock<T>(path: string, fn: () => Promise<T>): Prom...
function mergeAccountStorage (line 402) | function mergeAccountStorage(
function deduplicateAccountsByEmail (line 444) | function deduplicateAccountsByEmail<
function migrateV1ToV2 (line 509) | function migrateV1ToV2(v1: AccountStorageV1): AccountStorage {
function migrateV2ToV3 (line 540) | function migrateV2ToV3(v2: AccountStorage): AccountStorageV3 {
function migrateV3ToV4 (line 576) | function migrateV3ToV4(v3: AccountStorageV3): AccountStorageV4 {
function loadAccounts (line 589) | async function loadAccounts(): Promise<AccountStorageV4 | null> {
function saveAccounts (line 693) | async function saveAccounts(storage: AccountStorageV4): Promise<void> {
function saveAccountsReplace (line 726) | async function saveAccountsReplace(storage: AccountStorageV4): Promise<v...
function loadAccountsUnsafe (line 750) | async function loadAccountsUnsafe(): Promise<AccountStorageV4 | null> {
function clearAccounts (line 782) | async function clearAccounts(): Promise<void> {
FILE: src/plugin/stores/signature-store.ts
function createSignatureStore (line 3) | function createSignatureStore(): SignatureStore {
function createThoughtBuffer (line 18) | function createThoughtBuffer(): ThoughtBuffer {
FILE: src/plugin/thinking-recovery.ts
type ConversationState (line 19) | interface ConversationState {
function isThinkingPart (line 41) | function isThinkingPart(part: any): boolean {
function isFunctionResponsePart (line 53) | function isFunctionResponsePart(part: any): boolean {
function isFunctionCallPart (line 60) | function isFunctionCallPart(part: any): boolean {
function isToolResultMessage (line 67) | function isToolResultMessage(msg: any): boolean {
function messageHasThinking (line 76) | function messageHasThinking(msg: any): boolean {
function messageHasToolCalls (line 98) | function messageHasToolCalls(msg: any): boolean {
function analyzeConversationState (line 125) | function analyzeConversationState(contents: any[]): ConversationState {
function stripAllThinkingBlocks (line 189) | function stripAllThinkingBlocks(contents: any[]): any[] {
function countTrailingToolResults (line 224) | function countTrailingToolResults(contents: any[]): number {
function closeToolLoopForThinking (line 264) | function closeToolLoopForThinking(contents: any[]): any[] {
function needsThinkingRecovery (line 305) | function needsThinkingRecovery(state: ConversationState): boolean {
function looksLikeCompactedThinkingTurn (line 330) | function looksLikeCompactedThinkingTurn(msg: any): boolean {
function hasPossibleCompactedThinking (line 381) | function hasPossibleCompactedThinking(
FILE: src/plugin/token.test.ts
function createClient (line 14) | function createClient() {
FILE: src/plugin/token.ts
type OAuthErrorPayload (line 10) | interface OAuthErrorPayload {
function parseOAuthErrorPayload (line 24) | function parseOAuthErrorPayload(text: string | undefined): { code?: stri...
class AntigravityTokenRefreshError (line 60) | class AntigravityTokenRefreshError extends Error {
method constructor (line 66) | constructor(options: {
function refreshAccessToken (line 85) | async function refreshAccessToken(
FILE: src/plugin/transform/claude.ts
constant CLAUDE_THINKING_MAX_OUTPUT_TOKENS (line 18) | const CLAUDE_THINKING_MAX_OUTPUT_TOKENS = 64_000;
constant CLAUDE_INTERLEAVED_THINKING_HINT (line 21) | const CLAUDE_INTERLEAVED_THINKING_HINT =
function isClaudeModel (line 27) | function isClaudeModel(model: string): boolean {
function isClaudeThinkingModel (line 34) | function isClaudeThinkingModel(model: string): boolean {
function configureClaudeToolConfig (line 43) | function configureClaudeToolConfig(payload: RequestPayload): void {
function buildClaudeThinkingConfig (line 62) | function buildClaudeThinkingConfig(
function ensureClaudeMaxOutputTokens (line 78) | function ensureClaudeMaxOutputTokens(
function appendClaudeThinkingHint (line 96) | function appendClaudeThinkingHint(
function normalizeClaudeTools (line 146) | function normalizeClaudeTools(
function convertStopSequences (line 293) | function convertStopSequences(
type ClaudeTransformOptions (line 305) | interface ClaudeTransformOptions {
type ClaudeTransformResult (line 316) | interface ClaudeTransformResult {
function applyClaudeTransforms (line 324) | function applyClaudeTransforms(
FILE: src/plugin/transform/cross-model-sanitizer.ts
type ModelFamily (line 13) | type ModelFamily = "claude" | "gemini" | "unknown";
type SanitizerOptions (line 15) | interface SanitizerOptions {
type SanitizationResult (line 21) | interface SanitizationResult {
constant GEMINI_SIGNATURE_FIELDS (line 27) | const GEMINI_SIGNATURE_FIELDS = ["thoughtSignature", "thinkingMetadata"]...
constant CLAUDE_SIGNATURE_FIELDS (line 28) | const CLAUDE_SIGNATURE_FIELDS = ["signature"] as const;
function getModelFamily (line 30) | function getModelFamily(model: string): ModelFamily {
function isPlainObject (line 36) | function isPlainObject(value: unknown): value is Record<string, unknown> {
function stripGeminiThinkingMetadata (line 40) | function stripGeminiThinkingMetadata(
function stripClaudeThinkingFields (line 81) | function stripClaudeThinkingFields(
function sanitizePart (line 105) | function sanitizePart(
function sanitizeParts (line 128) | function sanitizeParts(
function sanitizeContents (line 144) | function sanitizeContents(
function sanitizeMessages (line 172) | function sanitizeMessages(
function deepSanitizeCrossModelMetadata (line 200) | function deepSanitizeCrossModelMetadata(
function sanitizeCrossModelPayload (line 262) | function sanitizeCrossModelPayload(
function sanitizeCrossModelPayloadInPlace (line 290) | function sanitizeCrossModelPayloadInPlace(
FILE: src/plugin/transform/gemini.ts
constant UNSUPPORTED_SCHEMA_FIELDS (line 27) | const UNSUPPORTED_SCHEMA_FIELDS = new Set([
function toGeminiSchema (line 52) | function toGeminiSchema(schema: unknown): unknown {
function isGeminiModel (line 129) | function isGeminiModel(model: string): boolean {
function isGemini3Model (line 137) | function isGemini3Model(model: string): boolean {
function isGemini25Model (line 144) | function isGemini25Model(model: string): boolean {
function isImageGenerationModel (line 152) | function isImageGenerationModel(model: string): boolean {
function buildGemini3ThinkingConfig (line 163) | function buildGemini3ThinkingConfig(
function buildGemini25ThinkingConfig (line 176) | function buildGemini25ThinkingConfig(
type ImageConfig (line 191) | interface ImageConfig {
constant VALID_ASPECT_RATIOS (line 198) | const VALID_ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "...
function buildImageGenerationConfig (line 210) | function buildImageGenerationConfig(): ImageConfig {
function normalizeGeminiTools (line 230) | function normalizeGeminiTools(
type GeminiTransformOptions (line 347) | interface GeminiTransformOptions {
type GeminiTransformResult (line 360) | interface GeminiTransformResult {
function applyGeminiTransforms (line 372) | function applyGeminiTransforms(
type WrapToolsResult (line 434) | interface WrapToolsResult {
function isWebSearchTool (line 456) | function isWebSearchTool(tool: Record<string, unknown>): boolean {
function wrapToolsAsFunctionDeclarations (line 476) | function wrapToolsAsFunctionDeclarations(payload: RequestPayload): WrapT...
FILE: src/plugin/transform/model-resolver.ts
type ModelResolverOptions (line 10) | interface ModelResolverOptions {
constant THINKING_TIER_BUDGETS (line 18) | const THINKING_TIER_BUDGETS = {
constant GEMINI_3_THINKING_LEVELS (line 30) | const GEMINI_3_THINKING_LEVELS = ["minimal", "low", "medium", "high"] as...
constant MODEL_ALIASES (line 40) | const MODEL_ALIASES: Record<string, string> = {
constant TIER_REGEX (line 62) | const TIER_REGEX = /-(minimal|low|medium|high)$/;
constant QUOTA_PREFIX_REGEX (line 63) | const QUOTA_PREFIX_REGEX = /^antigravity-/i;
constant GEMINI_3_PRO_REGEX (line 64) | const GEMINI_3_PRO_REGEX = /^gemini-3(?:\.\d+)?-pro/i;
constant GEMINI_3_FLASH_REGEX (line 65) | const GEMINI_3_FLASH_REGEX = /^gemini-3(?:\.\d+)?-flash/i;
constant IMAGE_GENERATION_MODELS (line 73) | const IMAGE_GENERATION_MODELS = /image|imagen/i;
function supportsThinkingTiers (line 82) | function supportsThinkingTiers(model: string): boolean {
function extractThinkingTierFromModel (line 95) | function extractThinkingTierFromModel(model: string): ThinkingTier | und...
function getBudgetFamily (line 107) | function getBudgetFamily(model: string): keyof typeof THINKING_TIER_BUDG...
function isThinkingCapableModel (line 123) | function isThinkingCapableModel(model: string): boolean {
function isGemini3ProModel (line 132) | function isGemini3ProModel(model: string): boolean {
function isGemini3FlashModel (line 136) | function isGemini3FlashModel(model: string): boolean {
function resolveModelWithTier (line 160) | function resolveModelWithTier(requestedModel: string, options: ModelReso...
function getModelFamily (line 272) | function getModelFamily(model: string): "claude" | "gemini-flash" | "gem...
type VariantConfig (line 286) | interface VariantConfig {
function budgetToGemini3Level (line 295) | function budgetToGemini3Level(budget: number): "low" | "medium" | "high" {
function resolveModelForHeaderStyle (line 310) | function resolveModelForHeaderStyle(
function resolveModelWithVariant (line 363) | function resolveModelWithVariant(
FILE: src/plugin/transform/types.ts
type ModelFamily (line 3) | type ModelFamily = "claude" | "gemini-flash" | "gemini-pro";
type ThinkingTier (line 5) | type ThinkingTier = "low" | "medium" | "high";
type TransformContext (line 11) | interface TransformContext {
type TransformResult (line 37) | interface TransformResult {
type TransformDebugInfo (line 47) | interface TransformDebugInfo {
type RequestPayload (line 66) | type RequestPayload = Record<string, unknown>;
type ThinkingConfig (line 71) | interface ThinkingConfig {
type GoogleSearchConfig (line 89) | interface GoogleSearchConfig {
type ResolvedModel (line 98) | interface ResolvedModel {
FILE: src/plugin/types.ts
type OAuthAuthDetails (line 4) | interface OAuthAuthDetails {
type ApiKeyAuthDetails (line 11) | interface ApiKeyAuthDetails {
type NonOAuthAuthDetails (line 16) | interface NonOAuthAuthDetails {
type AuthDetails (line 21) | type AuthDetails = OAuthAuthDetails | ApiKeyAuthDetails | NonOAuthAuthDe...
type GetAuth (line 23) | type GetAuth = () => Promise<AuthDetails>;
type ProviderModel (line 25) | interface ProviderModel {
type Provider (line 33) | interface Provider {
type LoaderResult (line 37) | interface LoaderResult {
type PluginClient (line 42) | type PluginClient = PluginInput["client"];
type PluginContext (line 44) | interface PluginContext {
type AuthPrompt (line 49) | type AuthPrompt =
type OAuthAuthorizationResult (line 66) | type OAuthAuthorizationResult = { url: string; instructions: string } & (
type AuthMethod (line 77) | interface AuthMethod {
type PluginEventPayload (line 85) | interface PluginEventPayload {
type PluginResult (line 92) | interface PluginResult {
type RefreshParts (line 102) | interface RefreshParts {
type ProjectContextResult (line 108) | interface ProjectContextResult {
FILE: src/plugin/ui/ansi.ts
constant ANSI (line 6) | const ANSI = {
type KeyAction (line 27) | type KeyAction = 'up' | 'down' | 'enter' | 'escape' | 'escape-start' | n...
function parseKey (line 33) | function parseKey(data: Buffer): KeyAction {
function isTTY (line 55) | function isTTY(): boolean {
FILE: src/plugin/ui/auth-menu.test.ts
function formatRelativeTime (line 4) | function formatRelativeTime(timestamp: number | undefined): string {
function formatDate (line 14) | function formatDate(timestamp: number | undefined): string {
type AccountStatus (line 19) | type AccountStatus = 'active' | 'rate-limited' | 'expired' | 'unknown';
function getStatusBadge (line 21) | function getStatusBadge(status: AccountStatus | undefined): string {
FILE: src/plugin/ui/auth-menu.ts
type AccountStatus (line 5) | type AccountStatus = 'active' | 'rate-limited' | 'expired' | 'verificati...
type AccountInfo (line 7) | interface AccountInfo {
type AuthMenuAction (line 17) | type AuthMenuAction =
type AccountAction (line 27) | type AccountAction = 'back' | 'delete' | 'refresh' | 'toggle' | 'verify'...
function formatRelativeTime (line 29) | function formatRelativeTime(timestamp: number | undefined): string {
function formatDate (line 39) | function formatDate(timestamp: number | undefined): string {
function getStatusBadge (line 44) | function getStatusBadge(status: AccountStatus | undefined): string {
function showAuthMenu (line 54) | async function showAuthMenu(accounts: AccountInfo[]): Promise<AuthMenuAc...
function showAccountDetails (line 106) | async function showAccountDetails(account: AccountInfo): Promise<Account...
FILE: src/plugin/ui/confirm.ts
function confirm (line 3) | async function confirm(message: string, defaultYes = false): Promise<boo...
FILE: src/plugin/ui/select.ts
type MenuItem (line 3) | interface MenuItem<T = string> {
type SelectOptions (line 14) | interface SelectOptions {
constant ESCAPE_TIMEOUT_MS (line 26) | const ESCAPE_TIMEOUT_MS = 50;
constant ANSI_REGEX (line 28) | const ANSI_REGEX = new RegExp("\\x1b\\[[0-9;]*m", "g");
constant ANSI_LEADING_REGEX (line 29) | const ANSI_LEADING_REGEX = new RegExp("^\\x1b\\[[0-9;]*m");
function stripAnsi (line 31) | function stripAnsi(input: string): string {
function truncateAnsi (line 35) | function truncateAnsi(input: string, maxVisibleChars: number): string {
function getColorCode (line 71) | function getColorCode(color: MenuItem['color']): string {
function select (line 81) | async function select<T>(
FILE: src/plugin/version.ts
constant VERSION_URL (line 18) | const VERSION_URL = "https://antigravity-auto-updater-974169037036.us-ce...
constant CHANGELOG_URL (line 19) | const CHANGELOG_URL = "https://antigravity.google/changelog";
constant FETCH_TIMEOUT_MS (line 20) | const FETCH_TIMEOUT_MS = 5000;
constant CHANGELOG_SCAN_CHARS (line 21) | const CHANGELOG_SCAN_CHARS = 5000;
constant VERSION_REGEX (line 22) | const VERSION_REGEX = /\d+\.\d+\.\d+/;
type VersionSource (line 24) | type VersionSource = "api" | "changelog" | "fallback";
function parseVersion (line 26) | function parseVersion(text: string): string | null {
function tryFetchVersion (line 31) | async function tryFetchVersion(url: string, maxChars?: number): Promise<...
function initAntigravityVersion (line 51) | async function initAntigravityVersion(): Promise<void> {
FILE: src/shims.d.ts
type PkcePair (line 2) | interface PkcePair {
Condensed preview — 127 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,442K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 824,
"preview": "# These are supported funding model platforms\n\ngithub: NoeFabris\npatreon: # Replace with a single Patreon username\nopen_"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 6809,
"preview": "name: Bug Report\ndescription: Report a bug or issue with the plugin\nlabels: [\"bug\", \"needs-triage\"]\nbody:\n - type: mark"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 738,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: Troubleshooting Guide\n url: https://github.com/NoeFabris/opencod"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 2655,
"preview": "name: Feature Request\ndescription: Suggest a new feature or enhancement\nlabels: [\"enhancement\", \"needs-triage\"]\nbody:\n "
},
{
"path": ".github/workflows/ci.yml",
"chars": 563,
"preview": "name: CI\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n test:\n name: Test on Node.js"
},
{
"path": ".github/workflows/issue-triage.yml",
"chars": 11176,
"preview": "name: '🏷️ Issue Triage'\n\non:\n issues:\n types:\n - 'opened'\n - 'reopened'\n issue_comment:\n types:\n "
},
{
"path": ".github/workflows/release-beta.yml",
"chars": 9516,
"preview": "name: Release Beta\n\non:\n workflow_dispatch:\n inputs:\n force:\n description: 'Force publish even if versio"
},
{
"path": ".github/workflows/release.yml",
"chars": 7529,
"preview": "name: Release\n\non:\n push:\n branches:\n - main\n workflow_dispatch:\n\npermissions:\n contents: write\n id-token: w"
},
{
"path": ".github/workflows/republish-version.yml",
"chars": 6908,
"preview": "name: Republish Version\n\non:\n workflow_dispatch:\n inputs:\n version:\n description: 'Version to republish "
},
{
"path": ".github/workflows/update-dist-tag.yml",
"chars": 2536,
"preview": "name: Update NPM Dist Tag\n\non:\n workflow_dispatch:\n inputs:\n version:\n description: 'Version to tag (e.g"
},
{
"path": ".gitignore",
"chars": 603,
"preview": "# dependencies (bun install)\nnode_modules\n\n# output\nout\ndist\n*.tgz\n\n# code coverage\ncoverage\n*.lcov\n\n# logs\nlogs\n_.log\nr"
},
{
"path": "AGENTS.MD",
"chars": 6428,
"preview": "# AGENTS.md\n\nGuidance for AI agents working with this repository.\n\n## Overview\n\nOpenCode plugin for Google Antigravity O"
},
{
"path": "CHANGELOG.md",
"chars": 19239,
"preview": "# Changelog\n\n## [1.6.0] - 2026-02-20\n\n### Fixed\n\n- **#397** - Gemini tool-call payload handling now enforces valid `thou"
},
{
"path": "LICENSE",
"chars": 1061,
"preview": "MIT License\n\nCopyright (c) 2025 Jens\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof th"
},
{
"path": "README.md",
"chars": 23934,
"preview": "# Antigravity + Gemini CLI OAuth Plugin for Opencode\n\n[:\n\n```jso"
},
{
"path": "docs/MODEL-VARIANTS.md",
"chars": 5148,
"preview": "# Model Variants\n\nOpenCode's variant system allows you to configure thinking budget dynamically instead of defining sepa"
},
{
"path": "docs/MULTI-ACCOUNT.md",
"chars": 5544,
"preview": "# Multi-Account Setup\n\nAdd multiple Google accounts to increase your combined quota. The plugin automatically rotates be"
},
{
"path": "docs/TROUBLESHOOTING.md",
"chars": 12828,
"preview": "# Troubleshooting\n\nCommon issues and solutions for the Antigravity Auth plugin.\n\n> **Quick Reset**: Most issues can be r"
},
{
"path": "index.ts",
"chars": 284,
"preview": "export {\n AntigravityCLIOAuthPlugin,\n GoogleOAuthPlugin,\n} from \"./src/plugin\";\n\nexport {\n authorizeAntigravity,\n ex"
},
{
"path": "package.json",
"chars": 1987,
"preview": "{\n \"name\": \"opencode-antigravity-auth\",\n \"version\": \"1.6.0\",\n \"description\": \"Google Antigravity IDE OAuth auth"
},
{
"path": "script/build-schema.ts",
"chars": 5326,
"preview": "import { writeFileSync, mkdirSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } "
},
{
"path": "script/test-cross-model-e2e.sh",
"chars": 9375,
"preview": "#!/bin/bash\n# Cross-Model E2E Test Suite - 5 Model Variants\n# Tests fix for \"Invalid `signature` in `thinking` block\" er"
},
{
"path": "script/test-cross-model.ts",
"chars": 4251,
"preview": "#!/usr/bin/env npx tsx\nimport {\n sanitizeCrossModelPayload,\n getModelFamily,\n} from '../src/plugin/transform/cross-mod"
},
{
"path": "script/test-gemini-cli-e2e.sh",
"chars": 5264,
"preview": "#!/bin/bash\n# Gemini CLI E2E Test Suite\n# Tests gemini-cli models routing through cloudcode-pa.googleapis.com/v1internal"
},
{
"path": "script/test-models.ts",
"chars": 5345,
"preview": "#!/usr/bin/env npx tsx\nimport { spawn } from \"child_process\";\n\ninterface ModelTest {\n model: string;\n category: \"gemin"
},
{
"path": "script/test-regression.ts",
"chars": 21337,
"preview": "#!/usr/bin/env npx tsx\nimport { spawn } from \"child_process\";\n\ntype Category = \"thinking-order\" | \"tool-pairing\" | \"mult"
},
{
"path": "scripts/README-PI.md",
"chars": 1378,
"preview": "# Raspberry Pi Runner Setup\n\nUse your Raspberry Pi as a persistent, self-hosted runner for Opencode Triage. This enables"
},
{
"path": "scripts/auth-pi-tools.sh",
"chars": 1214,
"preview": "#!/bin/bash\nset -e\n\n# Colors\nGREEN='\\033[0;32m'\nBLUE='\\033[0;34m'\nNC='\\033[0m'\n\necho -e \"${BLUE}=== Authenticating Devel"
},
{
"path": "scripts/check-quota.mjs",
"chars": 7102,
"preview": "import { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nconst CLIE"
},
{
"path": "scripts/setup-opencode-pi.sh",
"chars": 1916,
"preview": "#!/bin/bash\nset -e\n\n# Colors\nGREEN='\\033[0;32m'\nBLUE='\\033[0;34m'\nYELLOW='\\033[0;33m'\nNC='\\033[0m'\n\necho -e \"${BLUE}=== "
},
{
"path": "scripts/setup-pi-runner.sh",
"chars": 3025,
"preview": "#!/bin/bash\nset -e\n\n# Colors for output\nGREEN='\\033[0;32m'\nBLUE='\\033[0;34m'\nNC='\\033[0m' # No Color\n\necho -e \"${BLUE}=="
},
{
"path": "src/antigravity/oauth.ts",
"chars": 7894,
"preview": "import { generatePKCE } from \"@openauthjs/openauth/pkce\";\n\nimport {\n ANTIGRAVITY_CLIENT_ID,\n ANTIGRAVITY_CLIENT_SECRET"
},
{
"path": "src/constants.test.ts",
"chars": 3127,
"preview": "import { describe, it, expect } from \"vitest\"\nimport {\n GEMINI_CLI_HEADERS,\n getRandomizedHeaders,\n type HeaderSet,\n}"
},
{
"path": "src/constants.ts",
"chars": 10641,
"preview": "/**\n * Constants used for Antigravity OAuth flows and Cloud Code Assist API integration.\n */\nexport const ANTIGRAVITY_CL"
},
{
"path": "src/hooks/auto-update-checker/cache.ts",
"chars": 2735,
"preview": "import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { CACHE_DIR, PACKAGE_NAME } from \"./constants\";"
},
{
"path": "src/hooks/auto-update-checker/checker.ts",
"chars": 8421,
"preview": "import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type "
},
{
"path": "src/hooks/auto-update-checker/constants.ts",
"chars": 1113,
"preview": "import * as path from \"node:path\";\nimport * as os from \"node:os\";\n\nexport const PACKAGE_NAME = \"opencode-antigravity-aut"
},
{
"path": "src/hooks/auto-update-checker/index.test.ts",
"chars": 8413,
"preview": "import { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nvi.mock(\"./checker\", () => ({\n getCachedVers"
},
{
"path": "src/hooks/auto-update-checker/index.ts",
"chars": 4981,
"preview": "import type { AutoUpdateCheckerOptions } from \"./types\";\nimport { getCachedVersion, getLocalDevVersion, findPluginEntry,"
},
{
"path": "src/hooks/auto-update-checker/logging.ts",
"chars": 345,
"preview": "import { debugLogToFile } from \"../../plugin/debug\";\n\nconst AUTO_UPDATE_LOG_PREFIX = \"[auto-update-checker]\";\n\nexport fu"
},
{
"path": "src/hooks/auto-update-checker/types.ts",
"chars": 530,
"preview": "export interface NpmDistTags {\n latest: string;\n [key: string]: string;\n}\n\nexport interface OpencodeConfig {\n plugin?"
},
{
"path": "src/plugin/accounts.test.ts",
"chars": 65193,
"preview": "import { beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { AccountManager, type ModelFamily, type HeaderSt"
},
{
"path": "src/plugin/accounts.ts",
"chars": 43676,
"preview": "import { formatRefreshParts, parseRefreshParts } from \"./auth\";\nimport { loadAccounts, saveAccounts, type AccountStorage"
},
{
"path": "src/plugin/antigravity-first-fallback.test.ts",
"chars": 8286,
"preview": "import { beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { AccountManager, type ModelFamily, type HeaderSt"
},
{
"path": "src/plugin/auth.test.ts",
"chars": 5576,
"preview": "import { beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { isOAuthAuth, parseRefreshParts, formatRefreshPa"
},
{
"path": "src/plugin/auth.ts",
"chars": 1855,
"preview": "import type { AuthDetails, OAuthAuthDetails, RefreshParts } from \"./types\";\n\nconst ACCESS_TOKEN_EXPIRY_BUFFER_MS = 60 * "
},
{
"path": "src/plugin/cache/index.ts",
"chars": 142,
"preview": "/**\n * Cache module for opencode-antigravity-auth plugin.\n */\n\nexport {\n SignatureCache,\n createSignatureCache,\n} from"
},
{
"path": "src/plugin/cache/signature-cache.ts",
"chars": 12762,
"preview": "/**\n * Signature cache for persisting thinking block signatures to disk.\n * \n * Features (based on LLM-API-Key-Proxy's P"
},
{
"path": "src/plugin/cache.test.ts",
"chars": 9676,
"preview": "import { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport {\n resolveCachedAuth,\n storeCachedAu"
},
{
"path": "src/plugin/cache.ts",
"chars": 7110,
"preview": "import { accessTokenExpired } from \"./auth\";\nimport type { OAuthAuthDetails } from \"./types\";\nimport { createHash } from"
},
{
"path": "src/plugin/cli.ts",
"chars": 4783,
"preview": "import { createInterface } from \"node:readline/promises\";\nimport { stdin as input, stdout as output } from \"node:process"
},
{
"path": "src/plugin/config/index.ts",
"chars": 616,
"preview": "/**\n * Configuration module for opencode-antigravity-auth plugin.\n * \n * @example\n * ```typescript\n * import { loadConfi"
},
{
"path": "src/plugin/config/loader.ts",
"chars": 4650,
"preview": "/**\n * Configuration loader for opencode-antigravity-auth plugin.\n * \n * Loads config from files.\n * Priority (lowest to"
},
{
"path": "src/plugin/config/models.test.ts",
"chars": 1808,
"preview": "import { describe, expect, it } from \"vitest\";\n\nimport { OPENCODE_MODEL_DEFINITIONS } from \"./models\";\n\nconst getModel ="
},
{
"path": "src/plugin/config/models.ts",
"chars": 3325,
"preview": "import type { ProviderModel } from \"../types\";\n\nexport type ModelThinkingLevel = \"minimal\" | \"low\" | \"medium\" | \"high\";\n"
},
{
"path": "src/plugin/config/schema.test.ts",
"chars": 1889,
"preview": "import { readFileSync } from \"node:fs\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { DEFAULT_CONFIG } from \""
},
{
"path": "src/plugin/config/schema.ts",
"chars": 17317,
"preview": "/**\n * Configuration schema for opencode-antigravity-auth plugin.\n * \n * Config file locations (in priority order, highe"
},
{
"path": "src/plugin/config/updater.test.ts",
"chars": 10910,
"preview": "import { describe, test, expect, beforeEach, afterEach } from \"vitest\";\nimport * as fs from \"node:fs\";\nimport * as path "
},
{
"path": "src/plugin/config/updater.ts",
"chars": 5013,
"preview": "/**\n * OpenCode configuration file updater.\n *\n * Updates ~/.config/opencode/opencode.json(c) with plugin models.\n */\n\ni"
},
{
"path": "src/plugin/core/streaming/index.ts",
"chars": 56,
"preview": "export * from './types';\nexport * from './transformer';\n"
},
{
"path": "src/plugin/core/streaming/transformer.ts",
"chars": 11029,
"preview": "import type {\n SignatureStore,\n StreamingCallbacks,\n StreamingOptions,\n ThoughtBuffer,\n} from './types';\nimport { pr"
},
{
"path": "src/plugin/core/streaming/types.ts",
"chars": 1037,
"preview": "export interface SignedThinking {\n text: string;\n signature: string;\n}\n\nexport interface SignatureStore {\n get(sessio"
},
{
"path": "src/plugin/cross-model-integration.test.ts",
"chars": 14468,
"preview": "import { describe, it, expect } from \"vitest\";\nimport {\n sanitizeCrossModelPayload,\n getModelFamily,\n} from \"./transfo"
},
{
"path": "src/plugin/debug.test.ts",
"chars": 2459,
"preview": "import { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\"\nimport { DEFAULT_CONFIG } from \"./config\"\n\ncons"
},
{
"path": "src/plugin/debug.ts",
"chars": 15760,
"preview": "import { createWriteStream, mkdirSync, readdirSync, statSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:pa"
},
{
"path": "src/plugin/errors.ts",
"chars": 1469,
"preview": "/**\n * Custom error types for opencode-antigravity-auth plugin.\n * \n * Ported from LLM-API-Key-Proxy for robust error ha"
},
{
"path": "src/plugin/fingerprint.ts",
"chars": 5358,
"preview": "/**\n * Device Fingerprint Generator for Rate Limit Mitigation\n *\n * Ported from antigravity-claude-proxy PR #170\n * http"
},
{
"path": "src/plugin/image-saver.ts",
"chars": 2898,
"preview": "/**\n * Image Saving Utility\n * \n * Handles saving generated images to disk and returning file paths.\n */\n\nimport * as fs"
},
{
"path": "src/plugin/logger.test.ts",
"chars": 2041,
"preview": "import { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\"\nimport { DEFAULT_CONFIG } from \"./config\"\nimpor"
},
{
"path": "src/plugin/logger.ts",
"chars": 2838,
"preview": "/**\n * Structured Logger for Antigravity Plugin\n *\n * Logging behavior:\n * - debug controls file logs only (via debug.ts"
},
{
"path": "src/plugin/logging-utils.test.ts",
"chars": 3225,
"preview": "import { describe, expect, it, vi } from \"vitest\"\nimport {\n deriveDebugPolicy,\n formatAccountContextLabel,\n formatAcc"
},
{
"path": "src/plugin/logging-utils.ts",
"chars": 3011,
"preview": "export type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\"\n\nexport interface DebugPolicyInput {\n configDebug: boolean\n "
},
{
"path": "src/plugin/model-specific-quota.test.ts",
"chars": 3477,
"preview": "import { describe, it, expect, beforeEach } from \"vitest\";\nimport { AccountManager } from \"./accounts\";\nimport type { OA"
},
{
"path": "src/plugin/persist-account-pool.test.ts",
"chars": 12899,
"preview": "/**\n * Tests for persistAccountPool function\n * \n * Issue #89: Multi-account login overwrites existing accounts\n * Root "
},
{
"path": "src/plugin/project.ts",
"chars": 9205,
"preview": "import {\n getAntigravityHeaders,\n ANTIGRAVITY_ENDPOINT_FALLBACKS,\n ANTIGRAVITY_LOAD_ENDPOINTS,\n ANTIGRAVITY_DEFAULT_"
},
{
"path": "src/plugin/quota-fallback.test.ts",
"chars": 5764,
"preview": "import { beforeAll, describe, expect, it, vi } from \"vitest\";\nimport type { HeaderStyle, ModelFamily } from \"./accounts\""
},
{
"path": "src/plugin/quota.ts",
"chars": 11964,
"preview": "import {\n ANTIGRAVITY_ENDPOINT_PROD,\n getAntigravityHeaders,\n ANTIGRAVITY_PROVIDER_ID,\n} from \"../constants\";\nimport "
},
{
"path": "src/plugin/recovery/constants.ts",
"chars": 1610,
"preview": "/**\n * Constants for session recovery storage paths.\n * \n * Based on oh-my-opencode/src/hooks/session-recovery/constants"
},
{
"path": "src/plugin/recovery/index.ts",
"chars": 351,
"preview": "/**\n * Session recovery module for opencode-antigravity-auth.\n * \n * Provides recovery from:\n * - tool_result_missing: I"
},
{
"path": "src/plugin/recovery/storage.ts",
"chars": 10878,
"preview": "/**\n * Storage utilities for reading OpenCode's session data.\n * \n * Based on oh-my-opencode/src/hooks/session-recovery/"
},
{
"path": "src/plugin/recovery/types.ts",
"chars": 3229,
"preview": "/**\n * Types for session recovery.\n * \n * Based on oh-my-opencode/src/hooks/session-recovery/types.ts\n */\n\n// =========="
},
{
"path": "src/plugin/recovery.test.ts",
"chars": 5918,
"preview": "import { describe, it, expect } from \"vitest\";\nimport { detectErrorType, isRecoverableError } from \"./recovery\";\n\ndescri"
},
{
"path": "src/plugin/recovery.ts",
"chars": 15167,
"preview": "/**\n * Session recovery hook for handling recoverable errors.\n * \n * Supports:\n * - tool_result_missing: When ESC is pre"
},
{
"path": "src/plugin/refresh-queue.test.ts",
"chars": 5047,
"preview": "import { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { ProactiveRefreshQueue } from \"./refresh-queue\";\n"
},
{
"path": "src/plugin/refresh-queue.ts",
"chars": 8830,
"preview": "/**\n * Proactive Token Refresh Queue\n * \n * Ported from LLM-API-Key-Proxy's BackgroundRefresher.\n * \n * This module prov"
},
{
"path": "src/plugin/request-helpers.test.ts",
"chars": 63252,
"preview": "import { describe, expect, it } from \"vitest\";\n\nimport {\n isThinkingCapableModel,\n extractThinkingConfig,\n extractVar"
},
{
"path": "src/plugin/request-helpers.ts",
"chars": 91887,
"preview": "import { getKeepThinking } from \"./config\";\nimport { createLogger } from \"./logger\";\nimport { cacheSignature } from \"./c"
},
{
"path": "src/plugin/request.test.ts",
"chars": 45480,
"preview": "import { describe, it, expect, vi } from \"vitest\";\nimport {\n prepareAntigravityRequest,\n transformAntigravityResponse,"
},
{
"path": "src/plugin/request.ts",
"chars": 73092,
"preview": "import crypto from \"node:crypto\";\nimport {\n ANTIGRAVITY_ENDPOINT,\n GEMINI_CLI_ENDPOINT,\n GEMINI_CLI_HEADERS,\n EMPTY_"
},
{
"path": "src/plugin/rotation.test.ts",
"chars": 19190,
"preview": "import { beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport {\n HealthScoreTracker,\n TokenBucketTracker,\n a"
},
{
"path": "src/plugin/rotation.ts",
"chars": 13968,
"preview": "/**\n * Account Rotation System\n * \n * Implements advanced account selection algorithms:\n * - Health Score: Track account"
},
{
"path": "src/plugin/search.ts",
"chars": 8143,
"preview": "/**\n * Google Search Tool Implementation\n *\n * Due to Gemini API limitations, native search tools (googleSearch, urlCont"
},
{
"path": "src/plugin/server.ts",
"chars": 10862,
"preview": "import { createServer } from \"node:http\";\nimport { readFileSync, existsSync } from \"node:fs\";\n\nimport { ANTIGRAVITY_REDI"
},
{
"path": "src/plugin/storage.test.ts",
"chars": 16124,
"preview": "import { describe, expect, it, vi, beforeEach } from \"vitest\";\nimport {\n deduplicateAccountsByEmail,\n migrateV2ToV3,\n "
},
{
"path": "src/plugin/storage.ts",
"chars": 22138,
"preview": "import { promises as fs } from \"node:fs\";\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n appendFileSync,\n mk"
},
{
"path": "src/plugin/stores/signature-store.ts",
"chars": 799,
"preview": "import type { SignatureStore, SignedThinking, ThoughtBuffer } from '../core/streaming/types';\n\nexport function createSig"
},
{
"path": "src/plugin/thinking-recovery.ts",
"chars": 12511,
"preview": "/**\n * Thinking Recovery Module\n *\n * Minimal implementation for recovering from corrupted thinking state.\n * When Claud"
},
{
"path": "src/plugin/token.test.ts",
"chars": 2526,
"preview": "import { beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { ANTIGRAVITY_PROVIDER_ID } from \"../constants\";\n"
},
{
"path": "src/plugin/token.ts",
"chars": 4989,
"preview": "import { ANTIGRAVITY_CLIENT_ID, ANTIGRAVITY_CLIENT_SECRET } from \"../constants\";\nimport { formatRefreshParts, parseRefre"
},
{
"path": "src/plugin/transform/claude.test.ts",
"chars": 26307,
"preview": "import { describe, it, expect } from \"vitest\";\nimport {\n isClaudeModel,\n isClaudeThinkingModel,\n configureClaudeToolC"
},
{
"path": "src/plugin/transform/claude.ts",
"chars": 12143,
"preview": "/**\n * Claude-specific Request Transformations\n * \n * Handles Claude model-specific request transformations including:\n "
},
{
"path": "src/plugin/transform/cross-model-sanitizer.test.ts",
"chars": 17789,
"preview": "import { describe, it, expect } from \"vitest\";\nimport {\n getModelFamily,\n stripGeminiThinkingMetadata,\n stripClaudeTh"
},
{
"path": "src/plugin/transform/cross-model-sanitizer.ts",
"chars": 9214,
"preview": "/**\n * Cross-Model Metadata Sanitization\n *\n * Fixes: \"Invalid `signature` in `thinking` block\" error when switching mod"
},
{
"path": "src/plugin/transform/gemini.test.ts",
"chars": 51904,
"preview": "import { describe, it, expect, beforeEach, afterEach, vi } from \"vitest\";\nimport {\n isGeminiModel,\n isGemini3Model,\n "
},
{
"path": "src/plugin/transform/gemini.ts",
"chars": 18721,
"preview": "/**\n * Gemini-specific Request Transformations\n * \n * Handles Gemini model-specific request transformations including:\n "
},
{
"path": "src/plugin/transform/index.ts",
"chars": 1675,
"preview": "/**\n * Transform Module Index\n * \n * Re-exports transform functions and types for request transformation.\n */\n\n// Types\n"
},
{
"path": "src/plugin/transform/model-resolver.test.ts",
"chars": 14448,
"preview": "import { describe, it, expect } from \"vitest\";\nimport { resolveModelWithTier, resolveModelWithVariant, resolveModelForHe"
},
{
"path": "src/plugin/transform/model-resolver.ts",
"chars": 13696,
"preview": "/**\n * Model Resolution with Thinking Tier Support\n * \n * Resolves model names with tier suffixes (e.g., gemini-3-pro-hi"
},
{
"path": "src/plugin/transform/types.ts",
"chars": 3822,
"preview": "import type { HeaderStyle } from \"../../constants\";\n\nexport type ModelFamily = \"claude\" | \"gemini-flash\" | \"gemini-pro\";"
},
{
"path": "src/plugin/types.ts",
"chars": 2538,
"preview": "import type { PluginInput } from \"@opencode-ai/plugin\";\nimport type { AntigravityTokenExchangeResult } from \"../antigrav"
},
{
"path": "src/plugin/ui/ansi.test.ts",
"chars": 2500,
"preview": "import { describe, it, expect } from 'vitest';\nimport { parseKey, isTTY, ANSI } from './ansi';\n\ndescribe('ansi', () => {"
},
{
"path": "src/plugin/ui/ansi.ts",
"chars": 1421,
"preview": "/**\n * ANSI escape codes and key parsing for interactive CLI menus.\n * Works cross-platform (Windows/Mac/Linux).\n */\n\nex"
},
{
"path": "src/plugin/ui/auth-menu.test.ts",
"chars": 3489,
"preview": "import { describe, it, expect } from 'vitest';\nimport { ANSI } from './ansi';\n\nfunction formatRelativeTime(timestamp: nu"
},
{
"path": "src/plugin/ui/auth-menu.ts",
"chars": 5320,
"preview": "import { ANSI } from './ansi';\nimport { select, type MenuItem } from './select';\nimport { confirm } from './confirm';\n\ne"
},
{
"path": "src/plugin/ui/confirm.ts",
"chars": 419,
"preview": "import { select } from './select';\n\nexport async function confirm(message: string, defaultYes = false): Promise<boolean>"
},
{
"path": "src/plugin/ui/select.ts",
"chars": 9380,
"preview": "import { ANSI, isTTY, parseKey } from './ansi';\n\nexport interface MenuItem<T = string> {\n label: string;\n value: T;\n "
},
{
"path": "src/plugin/version.test.ts",
"chars": 4341,
"preview": "import { describe, it, expect, vi, beforeEach, afterEach } from \"vitest\"\n\n/**\n * Regression tests for the version fallba"
},
{
"path": "src/plugin/version.ts",
"chars": 2613,
"preview": "/**\n * Remote Antigravity version fetcher.\n *\n * Mirrors the Antigravity-Manager's version resolution strategy:\n * 1. "
},
{
"path": "src/plugin.ts",
"chars": 137657,
"preview": "import { exec } from \"node:child_process\";\nimport { tool } from \"@opencode-ai/plugin\";\nimport {\n ANTIGRAVITY_DEFAULT_PR"
},
{
"path": "src/shims.d.ts",
"chars": 173,
"preview": "declare module \"@openauthjs/openauth/pkce\" {\n interface PkcePair {\n challenge: string;\n verifier: string;\n }\n\n "
},
{
"path": "tsconfig.build.json",
"chars": 377,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"noEmit\": false,\n \"outDir\": \"dist\",\n \"declaration\": t"
},
{
"path": "tsconfig.json",
"chars": 873,
"preview": "{\n \"include\": [\"src/**/*\", \"scripts/**/*\"],\n \"exclude\": [\"node_modules\", \"dist\", \"temp_research\", \"script\"],\n \"compil"
},
{
"path": "vitest.config.ts",
"chars": 214,
"preview": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n test: {\n globals: true,\n environmen"
}
]
About this extraction
This page contains the full source code of the NoeFabris/opencode-antigravity-auth GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 127 files (1.3 MB), approximately 327.6k tokens, and a symbol index with 958 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.