Building Custom AI Development Tools with Antigravity and Gemini API
Learn how to integrate Gemini API with Antigravity IDE to build custom AI-powered development tools — from automated code review to documentation generation and test scaffolding, with production-ready implementation patterns.
Antigravity IDE ships with Gemini natively integrated, providing powerful chat and code completion features. However, standard IDE capabilities don't always address team-specific workflows or project-unique requirements that demand programmatic control.
This guide walks you through building custom AI development tools by directly calling the Gemini API — covering everything from architectural design patterns to production-ready implementation. Through three practical tools — automated code review, API documentation generation, and test case scaffolding — you'll gain the skills to design and build your own AI-driven development pipeline.
Target audience: Developers who use Antigravity daily and have working knowledge of TypeScript/Node.js
Prerequisites:
Antigravity IDE (latest version)
Node.js 20 or later
A Google AI Studio account with an API key
Setting Up the Gemini API Foundation
Obtaining Your API Key and Configuring the Environment
Start by obtaining an API key from Google AI Studio and integrating it securely into your project.
// .env.local (make sure this is in your .gitignore)GEMINI_API_KEY=YOUR_GEMINI_API_KEY// src/lib/gemini-client.tsimport { GoogleGenerativeAI } from "@google/generative-ai";const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);// Gemini 2.5 Pro for deep code analysisexport const codeModel = genAI.getGenerativeModel({ model: "gemini-2.5-pro", generationConfig: { temperature: 0.2, // Low temperature for precision in code tasks maxOutputTokens: 8192, },});// Gemini 2.5 Flash for high-speed processingexport const flashModel = genAI.getGenerativeModel({ model: "gemini-2.5-flash", generationConfig: { temperature: 0.3, maxOutputTokens: 4096, },});
Implementing Rate Limiting and Retry Logic
Robust rate limiting is essential for production use. Here's a reusable implementation with exponential backoff.
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Master design patterns and implementation techniques for custom AI development tools using Gemini API
✦Build three practical tools: automated code review, documentation generation, and test scaffolding
✦Gain production-grade knowledge including rate limiting, error handling, and cost optimization
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
This tool complements manual code reviews by analyzing Git diffs to automatically detect security risks, performance issues, and design concerns.
// src/tools/code-reviewer.tsimport { codeModel } from "../lib/gemini-client";import { rateLimiter } from "../lib/rate-limiter";import { execSync } from "child_process";interface ReviewResult { severity: "critical" | "warning" | "info"; file: string; line?: number; message: string; suggestion?: string;}interface ReviewReport { summary: string; issues: ReviewResult[]; score: number; // 0-100}const REVIEW_PROMPT = `You are a senior software engineer. Review the following code diff.## Review Criteria1. **Security**: Injection vulnerabilities, authentication gaps, exposed secrets2. **Performance**: N+1 queries, unnecessary re-renders, memory leaks3. **Design**: SOLID violations, mixed responsibilities, testability issues4. **Error Handling**: Unhandled exceptions, misleading error messages## Output FormatReturn a JSON array. Each element should follow this structure:{ "severity": "critical" | "warning" | "info", "file": "file path", "line": line number (if identifiable), "message": "description of the issue", "suggestion": "code fix example (if applicable)"}Include an overall score (0-100) and summary at the end.## Code Diff`;export async function reviewGitDiff( baseBranch: string = "main"): Promise<ReviewReport> { // Get the Git diff const diff = execSync(`git diff ${baseBranch}...HEAD`, { encoding: "utf-8", maxBuffer: 1024 * 1024, // 1MB }); if (!diff.trim()) { return { summary: "No changes detected.", issues: [], score: 100, }; } // Split large diffs into manageable chunks const chunks = splitDiffByFile(diff); const allIssues: ReviewResult[] = []; for (const chunk of chunks) { const result = await rateLimiter.execute(async () => { const response = await codeModel.generateContent( REVIEW_PROMPT + chunk ); return response.response.text(); }); const parsed = parseReviewResponse(result); allIssues.push(...parsed); } // Deduplicate and calculate score const uniqueIssues = deduplicateIssues(allIssues); const score = calculateScore(uniqueIssues); return { summary: generateSummary(uniqueIssues), issues: uniqueIssues, score, };}function splitDiffByFile(diff: string): string[] { // Split on "diff --git" boundaries, keeping chunks under ~4000 tokens const files = diff.split(/^diff --git /m).filter(Boolean); const chunks: string[] = []; let currentChunk = ""; for (const file of files) { if ((currentChunk + file).length > 12000) { if (currentChunk) chunks.push(currentChunk); currentChunk = file; } else { currentChunk += `diff --git ${file}`; } } if (currentChunk) chunks.push(currentChunk); return chunks;}function calculateScore(issues: ReviewResult[]): number { let score = 100; for (const issue of issues) { if (issue.severity === "critical") score -= 20; else if (issue.severity === "warning") score -= 5; else score -= 1; } return Math.max(0, score);}
Running from the CLI
// scripts/review.tsimport { reviewGitDiff } from "../src/tools/code-reviewer";async function main() { console.log("🔍 Starting AI code review...\n"); const report = await reviewGitDiff("main"); console.log(`📊 Score: ${report.score}/100`); console.log(`📝 Summary: ${report.summary}\n`); for (const issue of report.issues) { const icon = issue.severity === "critical" ? "🔴" : issue.severity === "warning" ? "🟡" : "🔵"; console.log(`${icon} [${issue.severity}] ${issue.file}:${issue.line || "?"}`); console.log(` ${issue.message}`); if (issue.suggestion) { console.log(` 💡 ${issue.suggestion}`); } console.log(); }}main().catch(console.error);// Run: npx tsx scripts/review.ts// Expected output:// 🔍 Starting AI code review...// 📊 Score: 75/100// 📝 Summary: 2 security warnings and 3 performance suggestions// 🔴 [critical] src/api/auth.ts:42// User input is not sanitized// 💡 Add validation using a zod schema
Tool 2: Automated API Documentation Generation
Generating Docs from TypeScript Type Definitions
This tool analyzes your existing codebase — specifically TypeScript type information and comments — to automatically produce developer-facing API documentation.
// src/tools/doc-generator.tsimport { codeModel } from "../lib/gemini-client";import { rateLimiter } from "../lib/rate-limiter";import * as fs from "fs";import * as path from "path";import { glob } from "glob";interface DocSection { title: string; description: string; parameters?: Array<{ name: string; type: string; required: boolean; description: string; }>; returnType?: string; examples: string[];}const DOC_PROMPT = `You are a technical writer. Generate developer-facing API documentationin Markdown format from the following TypeScript code.## Requirements1. Concise explanation of each function/class purpose2. Parameter types and descriptions in table format3. Return type and description4. At least one working code example5. Error cases and how to handle them6. Links to related functions/classes## Source Code`;export async function generateApiDocs( sourceDir: string, outputPath: string): Promise<void> { // Find all API route files const apiFiles = await glob(`${sourceDir}/**/route.ts`, { ignore: ["**/node_modules/**"], }); const sections: string[] = []; sections.push("# API Reference\n"); sections.push(`> Auto-generated: ${new Date().toISOString()}\n`); for (const file of apiFiles) { const content = fs.readFileSync(file, "utf-8"); const relativePath = path.relative(sourceDir, file); // Infer the API endpoint path const endpoint = relativePath .replace(/\/route\.ts$/, "") .replace(/\\/g, "/"); console.log(`📄 Generating docs: /${endpoint}`); const doc = await rateLimiter.execute(async () => { const response = await codeModel.generateContent( DOC_PROMPT + `\nFile: ${relativePath}\nEndpoint: /${endpoint}\n\n${content}` ); return response.response.text(); }); sections.push(`## \`/${endpoint}\`\n`); sections.push(doc); sections.push("\n---\n"); } // Also process type definition files const typeFiles = await glob(`${sourceDir}/**/types.ts`, { ignore: ["**/node_modules/**"], }); if (typeFiles.length > 0) { sections.push("# Type Definitions\n"); for (const file of typeFiles) { const content = fs.readFileSync(file, "utf-8"); const relativePath = path.relative(sourceDir, file); const doc = await rateLimiter.execute(async () => { const response = await codeModel.generateContent( `Document the following TypeScript type definitions.\n\n${content}` ); return response.response.text(); }); sections.push(`## ${relativePath}\n`); sections.push(doc); } } fs.writeFileSync(outputPath, sections.join("\n"), "utf-8"); console.log(`✅ Documentation generated: ${outputPath}`);}
Incremental Documentation Updates
Full scans are resource-intensive. Implement a diff-based mode that updates documentation only for changed files.
// src/tools/doc-updater.tsimport { execSync } from "child_process";import { generateApiDocs } from "./doc-generator";export async function updateChangedDocs( sourceDir: string, docPath: string): Promise<string[]> { // Get files changed in the most recent commit const changedFiles = execSync("git diff --name-only HEAD~1 HEAD", { encoding: "utf-8", }) .trim() .split("\n") .filter((f) => f.endsWith(".ts") && f.includes("api")); if (changedFiles.length === 0) { console.log("📝 No API changes — skipping documentation update"); return []; } console.log( `📝 Updating documentation for ${changedFiles.length} files...` ); // Update only affected sections (merge with existing docs) await generateApiDocs(sourceDir, docPath); return changedFiles;}// Run: npx tsx scripts/update-docs.ts// Expected output:// 📝 Updating documentation for 3 files...// 📄 Generating docs: /api/checkout// 📄 Generating docs: /api/verify-session// 📄 Generating docs: /api/webhook// ✅ Documentation generated: docs/api-reference.md
Tool 3: Automated Test Case Generation
Generating Tests from Source Code
This tool analyzes function implementations and generates boundary value tests, happy/unhappy path tests, and edge case coverage.
// src/tools/test-generator.tsimport { codeModel } from "../lib/gemini-client";import { rateLimiter } from "../lib/rate-limiter";import * as fs from "fs";const TEST_PROMPT = `You are a test engineer. Generate test code for the following TypeScriptfunction using Vitest format.## Requirements1. Happy path tests (basic input/output verification)2. Boundary value tests (empty strings, 0, null, undefined, max values)3. Error path tests (invalid inputs, exception verification)4. Edge case tests (concurrency, timeouts, large datasets)5. Write descriptive test names6. Use vi.mock() for external dependencies## OutputReturn only the test code (no explanations)## Source Code`;interface TestGenerationResult { sourceFile: string; testFile: string; testCount: number; content: string;}export async function generateTests( sourceFile: string): Promise<TestGenerationResult> { const source = fs.readFileSync(sourceFile, "utf-8"); const testFile = sourceFile .replace(/\.ts$/, ".test.ts") .replace(/\/src\//, "/tests/"); const testCode = await rateLimiter.execute(async () => { const response = await codeModel.generateContent( TEST_PROMPT + `\nFile: ${sourceFile}\n\n${source}` ); return response.response.text(); }); // Strip markdown code fences const cleanCode = testCode .replace(/```typescript\n?/g, "") .replace(/```\n?/g, "") .trim(); // Count test cases const testCount = (cleanCode.match(/\b(it|test)\s*\(/g) || []).length; // Create directory structure const dir = testFile.substring(0, testFile.lastIndexOf("/")); fs.mkdirSync(dir, { recursive: true }); // Write test file fs.writeFileSync(testFile, cleanCode, "utf-8"); return { sourceFile, testFile, testCount, content: cleanCode, };}// Batch generation: generate tests for all files in a directoryexport async function generateTestsForDirectory( dir: string): Promise<TestGenerationResult[]> { const { glob } = await import("glob"); const files = await glob(`${dir}/**/*.ts`, { ignore: [ "**/*.test.ts", "**/*.spec.ts", "**/node_modules/**", "**/*.d.ts", ], }); const results: TestGenerationResult[] = []; for (const file of files) { console.log(`🧪 Generating tests: ${file}`); const result = await generateTests(file); console.log(` ✅ ${result.testCount} test cases → ${result.testFile}`); results.push(result); } return results;}// Run: npx tsx scripts/generate-tests.ts src/lib/// Expected output:// 🧪 Generating tests: src/lib/rate-limiter.ts// ✅ 8 test cases → tests/lib/rate-limiter.test.ts// 🧪 Generating tests: src/lib/gemini-client.ts// ✅ 5 test cases → tests/lib/gemini-client.test.ts
Production Integration: CI/CD Pipeline
GitHub Actions Integration
Wire all three tools into your CI/CD pipeline so they run automatically on every pull request.
Keeping Gemini API costs under control while maintaining quality requires deliberate architectural choices.
Model tiering: Use gemini-2.5-flash for lightweight tasks (linting, format checks) and gemini-2.5-pro for deep analysis (security review, architecture evaluation)
Response caching: Cache analysis results locally and reuse them as long as the source file hasn't changed
// src/lib/response-cache.tsimport * as fs from "fs";import * as crypto from "crypto";const CACHE_DIR = ".ai-cache";export function getCachedResponse(input: string): string | null { const hash = crypto.createHash("sha256").update(input).digest("hex"); const cachePath = `${CACHE_DIR}/${hash}.json`; if (fs.existsSync(cachePath)) { const cached = JSON.parse(fs.readFileSync(cachePath, "utf-8")); // Only use cache entries less than 24 hours old if (Date.now() - cached.timestamp < 86_400_000) { return cached.response; } } return null;}export function cacheResponse(input: string, response: string): void { const hash = crypto.createHash("sha256").update(input).digest("hex"); fs.mkdirSync(CACHE_DIR, { recursive: true }); fs.writeFileSync( `${CACHE_DIR}/${hash}.json`, JSON.stringify({ timestamp: Date.now(), response }), "utf-8" );}
Batch processing: Combine multiple files into a single API call when they fit within the token limit
Diff-based processing: Avoid full scans by analyzing only files that have changed
Error Handling and Fallback Strategies
Production environments need robust error handling to account for API outages and transient failures.
// src/lib/error-handler.tstype FallbackStrategy = "skip" | "cache" | "simplified";interface ToolConfig { name: string; fallback: FallbackStrategy; timeout: number;}export async function executeWithFallback<T>( config: ToolConfig, primaryFn: () => Promise<T>, fallbackFn?: () => Promise<T>): Promise<T | null> { try { // Execute with timeout guard const result = await Promise.race([ primaryFn(), new Promise<never>((_, reject) => setTimeout( () => reject(new Error(`Timeout: ${config.name}`)), config.timeout ) ), ]); return result; } catch (error: any) { console.error(`❌ ${config.name} failed: ${error.message}`); switch (config.fallback) { case "cache": // Return cached result from previous run console.log("📦 Using cached result from previous run..."); return fallbackFn ? fallbackFn() : null; case "simplified": // Fall back to a lighter model console.log("⚡ Falling back to Flash model..."); return fallbackFn ? fallbackFn() : null; case "skip": default: console.log("⏭️ Skipping, moving to next task..."); return null; } }}// Usage example// const report = await executeWithFallback(// { name: "code-review", fallback: "simplified", timeout: 30000 },// () => reviewWithPro(diff),// () => reviewWithFlash(diff)// );
Security Considerations
When operating tools powered by Gemini API, keep these security practices front of mind.
API key management: Store keys in environment variables, never hardcode them. In CI/CD, use GitHub Secrets or Cloudflare Worker Secrets
Input sanitization: Before sending code diffs or type definitions to the API, filter out sensitive data like .env values, API keys, and personal information
Output validation: Always review AI-generated code (tests, documentation) before execution. Pay special attention to external API calls or filesystem operations in generated test code
// src/lib/sanitizer.tsconst SENSITIVE_PATTERNS = [ /(?:api[_-]?key|secret|password|token)\s*[:=]\s*['"][^'"]+['"]/gi, /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, /(?:AKIA|ABIA|ACCA|ASIA)[0-9A-Z]{16}/g, // AWS Access Key];export function sanitizeForApi(content: string): string { let sanitized = content; for (const pattern of SENSITIVE_PATTERNS) { sanitized = sanitized.replace(pattern, "[REDACTED]"); } return sanitized;}
A Note from an Indie Developer
Key Takeaways
In this guide, we built three custom AI development tools by integrating Antigravity IDE with the Gemini API: automated code review, API documentation generation, and test case scaffolding. Each tool can be woven into your daily development workflow for significant productivity gains.
The key takeaway is that these tools are designed to complement — not replace — human judgment. Discussing AI-flagged issues with your team and manually validating AI-generated tests creates a quality-driven development pipeline that's greater than the sum of its parts.
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.