Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e7de187
feat(providers): discover Antigravity account models
sadiksaifi Aug 17, 2026
efd58ae
feat(providers): rank Antigravity models
sadiksaifi Aug 17, 2026
4b74f1a
feat(config): map legacy Gemini model intent
sadiksaifi Aug 17, 2026
127096d
feat(providers): isolate Antigravity generation
sadiksaifi Aug 17, 2026
a55b8ea
feat(providers): register Antigravity CLI
sadiksaifi Aug 17, 2026
b810611
feat(meta): publish Antigravity provider metadata
sadiksaifi Aug 17, 2026
1e23cc2
feat(configure): recommend Antigravity account model
sadiksaifi Aug 17, 2026
6abcfd9
feat(config): migrate Gemini CLI configurations
sadiksaifi Aug 17, 2026
23eca4d
refactor(providers): remove Gemini CLI implementation
sadiksaifi Aug 17, 2026
df563fb
feat(providers): verify Antigravity isolation before generation
sadiksaifi Aug 17, 2026
c0978f6
feat(providers): preserve file-backed Antigravity auth
sadiksaifi Aug 17, 2026
20585cc
fix(providers): honor Antigravity model fallback policy
sadiksaifi Aug 17, 2026
af4d16d
test(providers): cover Antigravity failures and migration
sadiksaifi Aug 17, 2026
41fc304
docs: replace Gemini CLI with Antigravity
sadiksaifi Aug 17, 2026
3503214
fix(providers): probe Antigravity isolation without history
sadiksaifi Aug 17, 2026
cf888ff
fix(providers): validate Antigravity model before generation
sadiksaifi Aug 17, 2026
4d7ce93
fix(providers): clean isolated profiles on Windows
sadiksaifi Aug 18, 2026
c29fe20
fix(providers): preserve Antigravity API-key auth
sadiksaifi Aug 18, 2026
e3b0ddb
fix(config): migrate only the effective legacy config
sadiksaifi Aug 18, 2026
72bd422
fix(config): surface Antigravity migration failures
sadiksaifi Aug 18, 2026
d2fc28f
fix(providers): time out stalled Antigravity processes
sadiksaifi Aug 18, 2026
d492796
fix(config): persist legacy project migrations
sadiksaifi Aug 18, 2026
054e72d
fix(providers): clean partial Antigravity profiles
sadiksaifi Aug 18, 2026
40ad237
fix(providers): preserve Antigravity auth selection
sadiksaifi Aug 18, 2026
8bcdf6e
fix(config): migrate partial legacy overrides
sadiksaifi Aug 18, 2026
1d50087
fix(providers): await Antigravity termination
sadiksaifi Aug 18, 2026
fd46ce7
fix(schema): preserve Antigravity examples
sadiksaifi Aug 18, 2026
92f9f03
chore: merge latest main
sadiksaifi Aug 19, 2026
6c795ce
style(cli): format spinner actor
sadiksaifi Aug 19, 2026
a29695a
fix(providers): preserve Antigravity fallback order
sadiksaifi Aug 19, 2026
577344b
fix(providers): bound Antigravity termination
sadiksaifi Aug 19, 2026
f3eca74
test(providers): add live Antigravity isolation smoke
sadiksaifi Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 24 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A CLI tool that leverages AI to automatically generate semantically correct, con
- 🤖 **AI-Powered** - Analyzes diffs and understands the _intent_ of your changes
- 📝 **Conventional Commits** - Strictly adheres to [v1.0.0](https://www.conventionalcommits.org/en/v1.0.0/) specification
- 🎨 **Interactive TUI** - Beautiful prompts for staging, editing, and confirming
- 🔌 **Multiple Providers** - Claude Code, Gemini CLI, Codex, OpenCode, Pi, OpenRouter, OpenAI, Anthropic, Google AI Studio, Cerebras
- 🔌 **Multiple Providers** - Claude Code, Antigravity CLI, Codex, OpenCode, Pi, OpenRouter, OpenAI, Anthropic, Google AI Studio, Cerebras
- 🔐 **Secure** - API keys stored in keychain, never in config files

## Installation
Expand Down Expand Up @@ -98,7 +98,7 @@ Info:
ai-git

# Override provider for this run
ai-git --provider gemini-cli --model gemini-3-flash-preview
ai-git --provider antigravity-cli --model gemini-3.7-flash-low

# Use Codex with reasoning effort baked in
ai-git --provider codex --model gpt-5.6-luna-low
Expand All @@ -124,25 +124,32 @@ ai-git --dry-run -A

## Supported Providers

| Provider | ID | Type | Requirements |
| :--------------- | :----------------- | :--- | :-------------------------------------------------------------------------------------- |
| Claude Code | `claude-code` | CLI | [Install CLI](https://claude.com/claude-code) |
| Gemini CLI | `gemini-cli` | CLI | [Install CLI](https://ai.google.dev/gemini-api/docs/cli) |
| Codex | `codex` | CLI | [Install CLI](https://developers.openai.com/codex/cli) |
| OpenCode | `opencode` | CLI | [Install CLI](https://opencode.ai) |
| Pi | `pi` | CLI | [Install CLI](https://pi.dev) |
| OpenRouter | `openrouter` | API | [Get API Key](https://openrouter.ai/keys) |
| OpenAI | `openai` | API | [Get API Key](https://platform.openai.com/api-keys) |
| Google AI Studio | `google-ai-studio` | API | [Get API Key](https://aistudio.google.com/app/apikey) |
| Anthropic | `anthropic` | API | [Get API Key](https://console.anthropic.com/settings/keys) |
| Cerebras | `cerebras` | API | [Get API Key](https://cloud.cerebras.ai/) |
| Provider | ID | Type | Requirements |
| :--------------- | :------------------ | :--- | :-------------------------------------------------------------------------------------- |
| Claude Code | `claude-code` | CLI | [Install CLI](https://claude.com/claude-code) |
| Antigravity CLI | `antigravity-cli` | CLI | [Install CLI](https://antigravity.google/docs/cli/install) |
| Codex | `codex` | CLI | [Install CLI](https://developers.openai.com/codex/cli) |
| OpenCode | `opencode` | CLI | [Install CLI](https://opencode.ai) |
| Pi | `pi` | CLI | [Install CLI](https://pi.dev) |
| OpenRouter | `openrouter` | API | [Get API Key](https://openrouter.ai/keys) |
| OpenAI | `openai` | API | [Get API Key](https://platform.openai.com/api-keys) |
| Google AI Studio | `google-ai-studio` | API | [Get API Key](https://aistudio.google.com/app/apikey) |
| Anthropic | `anthropic` | API | [Get API Key](https://console.anthropic.com/settings/keys) |
| Cerebras | `cerebras` | API | [Get API Key](https://cloud.cerebras.ai/) |

Configure with `ai-git configure`

Codex defaults to `gpt-5.6-luna-low` and Claude Code defaults to `haiku` for fast,
repeatable commit-message generation. Pi, OpenCode, and API provider models are discovered at
runtime. Retired Codex configurations are migrated to the corresponding GPT-5.6 family while
preserving their supported effort level.
repeatable commit-message generation. Antigravity CLI, Pi, OpenCode, and API provider models are
discovered at runtime. Antigravity recommends the newest Gemini Flash Low model available to the
signed-in account. Existing Gemini CLI configurations are migrated to Antigravity CLI with a live,
account-compatible model. Retired Codex configurations are migrated to the corresponding GPT-5.6
family while preserving their supported effort level.

Antigravity generation runs with `--sandbox` in an empty workspace and an isolated temporary
profile. AI Git installs deny-all permissions and a deny-all tool hook for the invocation, disables
slash-command expansion, validates structured output, and removes the temporary conversation data
afterward.

## Configuration

Expand Down
2 changes: 1 addition & 1 deletion apps/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Reusable `fromPromise()` actors with factory pattern for test dependency injecti

Two modes with unified adapter interface:

- **CLI Mode** (`cli/`): Spawns installed binaries (`claude`, `gemini`, `codex`)
- **CLI Mode** (`cli/`): Spawns installed binaries (`claude`, `agy`, `codex`, `opencode`, `pi`)
- **API Mode** (`api/`): HTTP APIs via Vercel AI SDK (OpenRouter, OpenAI, Anthropic, Google AI Studio)

Provider registry in `registry.ts` defines available providers and models.
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/scripts/sync-schema-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ function updateProviderDescription(
if (providerId === "openai") providerLabel = "OpenAI";
if (providerId === "google-ai-studio") providerLabel = "Google AI Studio";
if (providerId === "anthropic") providerLabel = "Anthropic API";
if (providerId === "antigravity-cli") providerLabel = "Antigravity CLI";
Comment thread
sadiksaifi marked this conversation as resolved.

modelSchema.description = `${providerLabel} model ID (e.g., '${example}'). Models are fetched dynamically.`;

Expand All @@ -146,7 +147,6 @@ function updateStaticCLIModels(schema: Record<string, unknown>): void {
"Claude Code virtual model ID. Fable, Sonnet, and Opus support low, medium, high, xhigh, and max. Haiku has no effort level and is recommended for this latency-sensitive workload.",
codex:
"Codex CLI virtual model ID: '<base-model>-<effort>'. Supported effort levels depend on the model and include low, medium, high, xhigh, and max. Default: 'gpt-5.6-luna-low'. Ultra is not a model effort and is not supported.",
"gemini-cli": "Gemini CLI model to use. 'gemini-3-flash-preview' is recommended for speed.",
};

for (const provider of PROVIDERS.filter(
Expand Down
13 changes: 10 additions & 3 deletions apps/cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ export async function getProjectConfigPath(): Promise<string> {
/** Pending migration notice to display after the welcome screen. */
let pendingMigrationNotice: MigrationNotice | null = null;

export function queueMigrationNotice(notice: MigrationNotice): void {
pendingMigrationNotice = notice;
}

/**
* Show any pending migration notice and clear it.
* Call this after the welcome screen renders so `console.clear()` doesn't wipe it.
Expand Down Expand Up @@ -296,16 +300,19 @@ export function mergePromptConfig(
*/
export async function resolveConfigAsync(
cliOptions: Partial<ResolvedConfig>,
loaded?: { userConfig?: UserConfig; projectConfig?: UserConfig },
): Promise<ResolvedConfig> {
const userConfig = await loadUserConfig();
const projectConfig = await loadProjectConfig();
const userConfig = loaded ? loaded.userConfig : await loadUserConfig();
const projectConfig = loaded ? loaded.projectConfig : await loadProjectConfig();
const hasCompleteCliOptions = Boolean(cliOptions.provider && cliOptions.model);

// Config file must exist and be valid (setup wizard ensures this)
// We check userConfig primarily, but if projectConfig exists and is complete, that's fine too.
// However, usually we expect at least a user config to exist after setup.
if (
(!userConfig || !isConfigComplete(userConfig)) &&
(!projectConfig || !isConfigComplete(projectConfig))
(!projectConfig || !isConfigComplete(projectConfig)) &&
!hasCompleteCliOptions
) {
throw new Error("Configuration is incomplete. Please run: ai-git configure");
}
Expand Down
138 changes: 136 additions & 2 deletions apps/cli/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ async function createPathWithoutProviderCLI(): Promise<string> {
return binDir;
}

async function createPathWithFakeAntigravity(): Promise<string> {
const binDir = await createPathWithoutProviderCLI();
const executable = process.platform === "win32" ? "agy.cmd" : "agy";
const script =
process.platform === "win32"
? `@echo off\r\nif "%1"=="--version" (echo 1.1.13) else (echo {"status":"SUCCESS","command":{"data":{"models":[{"id":"gemini-3.7-flash-low","label":"Gemini 3.7 Flash (Low)"},{"id":"gemini-3.1-pro-low","label":"Gemini 3.1 Pro (Low)"}]}}})\r\n`
: `#!/bin/sh\nif [ "$1" = "--version" ]; then\n echo 1.1.13\nelse\n echo '{"status":"SUCCESS","command":{"data":{"models":[{"id":"gemini-3.7-flash-low","label":"Gemini 3.7 Flash (Low)"},{"id":"gemini-3.1-pro-low","label":"Gemini 3.1 Pro (Low)"}]}}}'\nfi\n`;
const executablePath = path.join(binDir, executable);
fs.writeFileSync(executablePath, script, { mode: 0o700 });
return binDir;
}

async function runCLI(args: string[], options: RunCLIOptions): Promise<RunCLIResult> {
const proc = spawn([process.execPath, "run", CLI_PATH, ...args], {
cwd: options.cwd,
Expand Down Expand Up @@ -321,11 +333,101 @@ describe("ai-git CLI", () => {
expect(backupFile).toBeDefined();
});

it("migrates an existing Gemini CLI config to Antigravity before dry-run", async () => {
const homeDir = createTestHome({
provider: "gemini-cli",
model: "gemini-3.1-pro-preview",
});
const providerPath = await createPathWithFakeAntigravity();
const repoDir = createGitRepo();
fs.writeFileSync(path.join(repoDir, "README.md"), "updated\n");

const result = await runCLI(["--dry-run", "-A"], {
cwd: repoDir,
homeDir,
pathEnv: providerPath,
});

expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("DRY RUN: SYSTEM PROMPT");
const configPath = path.join(homeDir, ".config", "ai-git", "config.json");
expect(JSON.parse(fs.readFileSync(configPath, "utf8"))).toMatchObject({
provider: "antigravity-cli",
model: "gemini-3.1-pro-low",
});
const backupFile = fs
.readdirSync(path.dirname(configPath))
.find((file) => file.startsWith("config.json.") && file.endsWith(".bak"));
expect(backupFile).toBeDefined();
});

it("does not migrate an inactive legacy global config beneath a valid project config", async () => {
const homeDir = createTestHome({
provider: "gemini-cli",
model: "gemini-3-flash-preview",
});
const noProviderPath = await createPathWithoutProviderCLI();
const repoDir = createGitRepo();
fs.writeFileSync(
path.join(repoDir, ".ai-git.json"),
JSON.stringify({ provider: "claude-code", model: "haiku" }),
);
fs.writeFileSync(path.join(repoDir, "README.md"), "updated\n");

const result = await runCLI(["--dry-run", "-A"], {
cwd: repoDir,
homeDir,
pathEnv: noProviderPath,
});

expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("DRY RUN: SYSTEM PROMPT");
expect(result.stderr).not.toContain("Antigravity CLI is required to migrate");
});

it("does not migrate an inactive legacy config beneath an explicit provider override", async () => {
const homeDir = createTestHome({
provider: "gemini-cli",
model: "gemini-3-flash-preview",
});
const noProviderPath = await createPathWithoutProviderCLI();
const repoDir = createGitRepo();
fs.writeFileSync(path.join(repoDir, "README.md"), "updated\n");

const result = await runCLI(
["--provider", "claude-code", "--model", "haiku", "--dry-run", "-A"],
{ cwd: repoDir, homeDir, pathEnv: noProviderPath },
);

expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("DRY RUN: SYSTEM PROMPT");
expect(result.stderr).not.toContain("Antigravity CLI is required to migrate");
});

it("prints actionable guidance when an active legacy migration cannot start", async () => {
const homeDir = createTestHome({
provider: "gemini-cli",
model: "gemini-3-flash-preview",
});
const noProviderPath = await createPathWithoutProviderCLI();
const repoDir = createGitRepo();

const result = await runCLI(["--dry-run", "-A"], {
cwd: repoDir,
homeDir,
pathEnv: noProviderPath,
});

expect(result.exitCode).toBe(1);
expect(result.stderr).toContain("Antigravity CLI is required to migrate");
expect(result.stderr).toContain("curl -fsSL https://antigravity.google/cli/install.sh | bash");
});

it.each([
["codex", "gpt-5.6-luna-low"],
["claude-code", "haiku"],
["gemini-cli", "gemini-3-flash-preview"],
])("runs a dry-run with the static %s adapter selection", async (provider, model) => {
["antigravity-cli", "gemini-3.7-flash-low"],
])("runs a dry-run with the %s adapter selection", async (provider, model) => {
const homeDir = createTestHome({ provider, model });
const noProviderPath = await createPathWithoutProviderCLI();
const repoDir = createGitRepo();
Expand Down Expand Up @@ -417,3 +519,35 @@ describe("ai-git CLI", () => {
},
);
});

it("persists and backs up an effective legacy project configuration", async () => {
const homeDir = createTestHome();
const providerPath = await createPathWithFakeAntigravity();
const repoDir = createGitRepo();
const projectConfigPath = path.join(repoDir, ".ai-git.json");
fs.writeFileSync(
projectConfigPath,
JSON.stringify({ provider: "gemini-cli", model: "gemini-3.1-pro-preview" }),
);
fs.writeFileSync(path.join(repoDir, "README.md"), "updated\n");

const result = await runCLI(["--dry-run", "-A"], {
cwd: repoDir,
homeDir,
pathEnv: providerPath,
});

expect(result.exitCode).toBe(0);
expect(JSON.parse(fs.readFileSync(projectConfigPath, "utf8"))).toMatchObject({
provider: "antigravity-cli",
model: "gemini-3.1-pro-low",
});
const backupFile = fs
.readdirSync(repoDir)
.find((file) => file.startsWith(".ai-git.json.") && file.endsWith(".bak"));
expect(backupFile).toBeDefined();
expect(JSON.parse(fs.readFileSync(path.join(repoDir, backupFile!), "utf8"))).toEqual({
provider: "gemini-cli",
model: "gemini-3.1-pro-preview",
});
});
12 changes: 6 additions & 6 deletions apps/cli/src/lib/error-display.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ describe("categorizeError", () => {

test("CLI adapter → cli-error", () => {
const cliAdapter: ProviderAdapter = {
providerId: "gemini-cli",
providerId: "antigravity-cli",
mode: "cli",
invoke: async () => "",
checkAvailable: async () => true,
};
const error = new Error("Gemini CLI error (exit code 1):\ncommand not found");
const error = new Error("Antigravity CLI error (exit code 1):\ncommand not found");
const result = categorizeError(error, cliAdapter);
expect(result.category).toBe("cli-error");
expect(result.providerName).toBe("Gemini CLI");
expect(result.providerName).toBe("Antigravity CLI");
});

test("no adapter → cli-error with fallback provider name", () => {
Expand Down Expand Up @@ -98,11 +98,11 @@ describe("displayAIError", () => {
const output = captureStderr(() =>
displayAIError({
category: "cli-error",
message: "Gemini CLI error (exit code 1):\ncommand not found",
providerName: "Gemini CLI",
message: "Antigravity CLI error (exit code 1):\ncommand not found",
providerName: "Antigravity CLI",
}),
);
expect(output).toContain("Gemini CLI error (exit code 1):");
expect(output).toContain("Antigravity CLI error (exit code 1):");
expect(output).toContain("command not found");
});
});
22 changes: 21 additions & 1 deletion apps/cli/src/lib/migration.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test";
import { migrateConfig, migrations } from "./migration.ts";
import { migrateConfig, migrateLegacyGeminiCliConfig, migrations } from "./migration.ts";

describe("migrations registry", () => {
it("should have unique IDs", () => {
Expand Down Expand Up @@ -157,3 +157,23 @@ describe("migrateConfig", () => {
expect(result.changes).toHaveLength(0);
});
});

describe("migrateLegacyGeminiCliConfig", () => {
it("updates the provider and model together from the live Antigravity catalog", async () => {
const result = await migrateLegacyGeminiCliConfig(
{ provider: "gemini-cli", model: "gemini-3.1-pro-preview" },
async () => [
{ id: "gemini-3.7-flash-low", name: "Gemini 3.7 Flash (Low)" },
{ id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro (Low)" },
],
);

expect(result).toEqual({
config: { provider: "antigravity-cli", model: "gemini-3.1-pro-low" },
changed: true,
changes: [
"Migrated provider 'gemini-cli' → 'antigravity-cli' and model 'gemini-3.1-pro-preview' → 'gemini-3.1-pro-low'",
],
});
});
});
27 changes: 27 additions & 0 deletions apps/cli/src/lib/migration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { log } from "@clack/prompts";
import pc from "picocolors";
import type { UserConfig } from "../config.ts";
import { selectAntigravityMigrationModel } from "../providers/cli/antigravity.ts";
import type { APIModelDefinition } from "../providers/types.ts";

// ─── MIGRATION REGISTRY ──────────────────────────────────────────────

Expand Down Expand Up @@ -158,6 +160,31 @@ export function migrateConfig(raw: Record<string, unknown>): MigrationResult {
return { config: config as UserConfig, changed: changes.length > 0, changes };
}

export async function migrateLegacyGeminiCliConfig(
config: UserConfig,
loadModels: () => Promise<APIModelDefinition[]>,
): Promise<MigrationResult> {
if (config.provider !== "gemini-cli" || typeof config.model !== "string") {
return { config, changed: false, changes: [] };
}

const models = await loadModels();
const migratedModel = selectAntigravityMigrationModel(config.model, models);
if (!migratedModel) {
throw new Error(
`Cannot migrate unsupported Gemini CLI model '${config.model}'. Run \`ai-git configure\` to select an Antigravity model.`,
);
}

return {
config: { ...config, provider: "antigravity-cli", model: migratedModel },
changed: true,
changes: [
`Migrated provider 'gemini-cli' → 'antigravity-cli' and model '${config.model}' → '${migratedModel}'`,
],
};
}

// ─── UTILITIES ────────────────────────────────────────────────────────

/**
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/lib/model-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export interface CachedModel {
id: string;
/** Human-readable display name */
name: string;
/** Account-compatible recommendation supplied by a dynamic provider */
isRecommended?: boolean;
/** For OpenRouter: the original provider (anthropic, openai, google) */
provider?: string;
/** Sort rank (lower = higher priority, used for featured models) */
Expand Down
Loading
Loading