Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion site/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ For AI assistants: if a user asks you to install or set up wigolo (for any agent

Install: `npx wigolo init --agents=<csv>` auto-wires the agent (writes its MCP config and, where supported, instructions). Init is unattended by default — no prompts, safe in scripts and CI. `--agents` is optional — omit it for engine-only setup and point any other MCP client at `npx wigolo mcp` by hand. Init is instant; the browser engine and on-device models download automatically on first use (`npx wigolo warmup --all` pre-caches them).
Supported agents (auto-wire): Claude Code, Cursor, Codex, Gemini CLI, OpenCode, VS Code, Windsurf, Zed, Antigravity. Any MCP-capable agent works via manual config.
Manual MCP setup (custom agent, or any agent not in the auto-wire list — e.g. an in-house agent): run `npx wigolo init` (engine-only setup, instant, unattended), then add wigolo to the agent's MCP config — command `npx` args `["-y","wigolo"]` (or command `wigolo` args `[]` if globally installed; bare `wigolo` starts the stdio MCP server, the default). Servers key by client: `mcpServers` (Cursor `~/.cursor/mcp.json`, Windsurf `~/.codeium/windsurf/mcp_config.json`, Gemini CLI `~/.gemini/settings.json`, Antigravity `~/.antigravity/mcp.json`), `servers` (VS Code user mcp.json), `context_servers` (Zed `~/.config/zed/settings.json`), `mcp` with `{ "type": "local", "command": ["npx", "-y", "wigolo"], "enabled": true }` (OpenCode `~/.config/opencode/opencode.json`), TOML `[mcp_servers.wigolo]` (Codex `~/.codex/config.toml`). Claude Code: `claude mcp add wigolo --scope user -- npx -y wigolo` (--scope user = global; omit for project-only). Add LLM keys via the server `env` block (e.g. WIGOLO_LLM_PROVIDER, GEMINI_API_KEY).
Manual MCP setup (custom agent, or any agent not in the auto-wire list — e.g. an in-house agent): run `npx wigolo init` (engine-only setup, instant, unattended), then add wigolo to the agent's MCP config — command `npx` args `["-y","wigolo"]` (or command `wigolo` args `[]` if globally installed; bare `wigolo` starts the stdio MCP server, the default). Servers key by client: `mcpServers` (Cursor `~/.cursor/mcp.json`, Windsurf `~/.codeium/windsurf/mcp_config.json`, Gemini CLI `~/.gemini/settings.json`, Antigravity `~/.gemini/config/mcp_config.json`), `servers` (VS Code user mcp.json), `context_servers` (Zed `~/.config/zed/settings.json`), `mcp` with `{ "type": "local", "command": ["npx", "-y", "wigolo"], "enabled": true }` (OpenCode `~/.config/opencode/opencode.json`), TOML `[mcp_servers.wigolo]` (Codex `~/.codex/config.toml`). Claude Code: `claude mcp add wigolo --scope user -- npx -y wigolo` (--scope user = global; omit for project-only). Add LLM keys via the server `env` block (e.g. WIGOLO_LLM_PROVIDER, GEMINI_API_KEY).
Optional answer synthesis (research/agent/answer-format search): set `WIGOLO_LLM_PROVIDER` plus its key/model — a provider alone is not enough. Easiest is a free Gemini key: `WIGOLO_LLM_PROVIDER=gemini` + `GEMINI_API_KEY` (free from aistudio.google.com; model defaults to gemini-2.5-flash-lite). Fully local & keyless: `WIGOLO_LLM_PROVIDER=ollama` + `WIGOLO_LLM_MODEL` with a running Ollama server. Also works: Anthropic/OpenAI/Groq keys.
Requirements: Node.js >= 20, ~1.5 GB disk, macOS/Linux/Windows.
Keyless core tools: search, fetch, crawl, extract, cache, find_similar (plus diff and watch).
Expand Down
50 changes: 31 additions & 19 deletions src/cli/agents/antigravity.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,59 @@
/**
* Antigravity integration.
*
* Antigravity is Anthropic's IDE (VS Code-derived), storing config in ~/.antigravity/.
* MCP config format mirrors VS Code's mcp.json (mcpServers key).
*
* TODO: When Antigravity's official MCP config docs are published, verify the
* exact config path and key structure.
* Antigravity is Google's IDE.
* The global MCP configuration is ~/.gemini/config/mcp_config.json.
* The file stores servers under the mcpServers key.
*/
import { existsSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { dirname, join } from 'node:path';
import { homedir } from 'node:os';
import { execSync } from 'node:child_process';
import { spawnSync } from 'node:child_process';
import { mergeMcpJson, removeMcpJson } from './utils.js';

const MCP_KEY_PATH = ['mcpServers', 'wigolo'];

function antigravityDir(): string {
return join(homedir(), '.antigravity');
export function antigravityDataDir(home: string = homedir()): string {
return join(home, '.gemini', 'antigravity');
}

function detect(): boolean {
if (existsSync(antigravityDir())) return true;
export function antigravityMcpConfigPath(home: string = homedir()): string {
return join(home, '.gemini', 'config', 'mcp_config.json');
}

function binaryExists(name: string): boolean {
const comand = process.platform === 'win32' ? 'where': 'which';

try {
execSync('which antigravity', { stdio: ['pipe', 'pipe', 'pipe'] });
return true;
const result = spawnSync(comand, [name], {
encoding:'utf-8',
timeout: 3000,
});

return !result.error && result.status === 0;
} catch {
return false;
}
}

function detect(): boolean {
return existsSync(antigravityDataDir()) ||
binaryExists('agy') ||
binaryExists('antigravity');
Comment thread
luojiyin1987 marked this conversation as resolved.
}

async function installMcp(cmd: { command: string; args: string[] }): Promise<void> {
const dir = antigravityDir();
mkdirSync(dir, { recursive: true });
const configPath = join(dir, 'mcp.json');
const configPath = antigravityMcpConfigPath();
mkdirSync(dirname(configPath), { recursive: true });
mergeMcpJson(configPath, { command: cmd.command, args: cmd.args }, MCP_KEY_PATH);
}

async function uninstall(): Promise<{ removed: string[] }> {
const removed: string[] = [];
const configPath = join(antigravityDir(), 'mcp.json');
const configPath = antigravityMcpConfigPath();
if (existsSync(configPath)) {
removeMcpJson(configPath, MCP_KEY_PATH);
removed.push('~/.antigravity/mcp.json (wigolo entry)');
removed.push('~/.gemini/config/mcp_config.json (wigolo entry)');
}
return { removed };
}
Expand All @@ -53,7 +65,7 @@ export const antigravityHandler = {
supportsCommands: false,
detect,
installMcp,
// No instructions layer for antigravity (config format unclear, MCP only)
// Antigravity has no instruction file in this integration.
installInstructions: async () => { /* noop */ },
uninstall,
};
8 changes: 5 additions & 3 deletions src/cli/tui/agents.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { join } from 'node:path';
import type { AgentDescriptor, DetectedAgent } from './agents-types.js';
import { binaryInPath, dirExists, fileExists, getHome, getCwd } from './detect-helpers.js';
import { antigravityDataDir, antigravityMcpConfigPath } from '../agents/antigravity.js';
import { vscodeUserDir } from '../agents/vscode.js';

export type { AgentId, AgentDescriptor, DetectedAgent, InstallType } from './agents-types.js';
Expand Down Expand Up @@ -94,9 +95,10 @@ const antigravity: AgentDescriptor = {
displayName: 'Antigravity',
installType: 'config-file',
detect: ({ home }) =>
binaryInPath('antigravity') !== null ||
dirExists(join(home, '.antigravity')),
configPath: ({ home }) => join(home, '.antigravity', 'mcp.json'),
dirExists(antigravityDataDir(home)) ||
binaryInPath('agy') !== null ||
binaryInPath('antigravity') !== null,
configPath: ({ home }) => antigravityMcpConfigPath(home),
};

export const AGENTS: readonly AgentDescriptor[] = [
Expand Down
101 changes: 101 additions & 0 deletions tests/unit/cli/agents/antigravity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';

vi.mock('node:os', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:os')>();
return { ...actual, homedir: vi.fn(() => tmpHome) };
});

vi.mock('node:child_process', () => ({
execSync: vi.fn(),
}));

import { execSync } from 'node:child_process';
import { homedir } from 'node:os';

let tmpHome: string;

beforeEach(() => {
tmpHome = join(tmpdir(), `wigolo-antigravity-test-${Date.now()}`);
mkdirSync(tmpHome, { recursive: true });
vi.mocked(homedir).mockReturnValue(tmpHome);
});

afterEach(() => {
rmSync(tmpHome, { recursive: true, force: true });
vi.clearAllMocks();
});

describe('antigravityHandler.detect', () => {
it('returns true when the Antigravity data directory exists', async () => {
mkdirSync(join(tmpHome, '.gemini', 'antigravity'), { recursive: true });
const { antigravityHandler } = await import('../../../../src/cli/agents/antigravity.js');
expect(antigravityHandler.detect()).toBe(true);
});

it('returns true when `agy` is on PATH', async () => {
vi.mocked(execSync).mockImplementation((command) => {
if (command === 'which agy') return Buffer.from('/usr/bin/agy');
throw new Error('not found');
});
const { antigravityHandler } = await import('../../../../src/cli/agents/antigravity.js');
expect(antigravityHandler.detect()).toBe(true);
});

it('returns false when no product signal exists', async () => {
mkdirSync(join(tmpHome, '.gemini', 'config'), { recursive: true });
vi.mocked(execSync).mockImplementation(() => { throw new Error('not found'); });
const { antigravityHandler } = await import('../../../../src/cli/agents/antigravity.js');
expect(antigravityHandler.detect()).toBe(false);
});
});

describe('antigravityHandler.installMcp', () => {
it('writes mcpServers.wigolo to the documented global config', async () => {
const { antigravityHandler } = await import('../../../../src/cli/agents/antigravity.js');
await antigravityHandler.installMcp({ command: 'npx', args: ['-y', 'wigolo'] });
const configPath = join(tmpHome, '.gemini', 'config', 'mcp_config.json');
const parsed = JSON.parse(readFileSync(configPath, 'utf-8'));
expect(parsed.mcpServers.wigolo).toEqual({
command: 'npx',
args: ['-y', 'wigolo'],
});
});

it('preserves other MCP server entries', async () => {
const configDir = join(tmpHome, '.gemini', 'config');
const configPath = join(configDir, 'mcp_config.json');
mkdirSync(configDir, { recursive: true });
writeFileSync(
configPath,
JSON.stringify({ mcpServers: { other: { command: 'other' } } }),
);
const { antigravityHandler } = await import('../../../../src/cli/agents/antigravity.js');
await antigravityHandler.installMcp({ command: 'npx', args: ['-y', 'wigolo'] });
const parsed = JSON.parse(readFileSync(configPath, 'utf-8'));
expect(parsed.mcpServers.other).toEqual({ command: 'other' });
expect(parsed.mcpServers.wigolo).toBeDefined();
});
});

describe('antigravityHandler.uninstall', () => {
it('removes only the wigolo MCP server entry', async () => {
const { antigravityHandler } = await import('../../../../src/cli/agents/antigravity.js');
await antigravityHandler.installMcp({ command: 'npx', args: ['-y', 'wigolo'] });
const configPath = join(tmpHome, '.gemini', 'config', 'mcp_config.json');
const before = JSON.parse(readFileSync(configPath, 'utf-8'));
before.mcpServers.other = { command: 'other' };
writeFileSync(configPath, JSON.stringify(before));

const result = await antigravityHandler.uninstall();
const parsed = JSON.parse(readFileSync(configPath, 'utf-8'));
expect(parsed.mcpServers.wigolo).toBeUndefined();
expect(parsed.mcpServers.other).toEqual({ command: 'other' });
expect(result.removed).toEqual([
'~/.gemini/config/mcp_config.json (wigolo entry)',
]);
expect(existsSync(configPath)).toBe(true);
});
});
21 changes: 16 additions & 5 deletions tests/unit/cli/tui/agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,18 +245,29 @@ describe('OpenCode descriptor', () => {
describe('Antigravity descriptor', () => {
beforeEach(() => vi.clearAllMocks());

it('detects when `antigravity` binary is on PATH', () => {
it('detects when `agy` binary is on PATH', () => {
vi.mocked(binaryInPath).mockImplementation((n) => (n === 'agy' ? '/usr/local/bin/agy' : null));
expect(getDescriptor('antigravity').detect(ENV)).toBe(true);
});

it('detects the legacy `antigravity` binary alias', () => {
vi.mocked(binaryInPath).mockImplementation((n) => (n === 'antigravity' ? '/usr/local/bin/antigravity' : null));
expect(getDescriptor('antigravity').detect(ENV)).toBe(true);
});

it('detects when ~/.antigravity dir exists', () => {
vi.mocked(dirExists).mockImplementation((p) => p === join('/home/test', '.antigravity'));
it('detects when ~/.gemini/antigravity dir exists', () => {
vi.mocked(dirExists).mockImplementation((p) => p === join('/home/test', '.gemini', 'antigravity'));
expect(getDescriptor('antigravity').detect(ENV)).toBe(true);
});

it('configPath returns ~/.antigravity/mcp.json', () => {
expect(getDescriptor('antigravity').configPath(ENV)).toBe(join('/home/test', '.antigravity', 'mcp.json'));
it('does not treat the shared ~/.gemini/config dir as an install signal', () => {
vi.mocked(dirExists).mockImplementation((p) => p === join('/home/test', '.gemini', 'config'));
vi.mocked(binaryInPath).mockReturnValue(null);
expect(getDescriptor('antigravity').detect(ENV)).toBe(false);
});

it('configPath returns ~/.gemini/config/mcp_config.json', () => {
expect(getDescriptor('antigravity').configPath(ENV)).toBe(join('/home/test', '.gemini', 'config', 'mcp_config.json'));
});
});

Expand Down