Skip to content
Closed
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
17 changes: 16 additions & 1 deletion docs/adapters/desktop/antigravity.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,27 @@ Start the Antigravity desktop app with the Chrome DevTools `remote-debugging-por

> Depending on your installation, the executable might be named differently, e.g., `Antigravity` instead of `Electron`.

Then set the target port:
Then either set the target port globally:

```bash
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
```

Or pass it per command, which is usually better when you switch between multiple desktop apps or multiple CDP ports:

```bash
opencli antigravity status --cdp-endpoint http://127.0.0.1:9224
opencli antigravity send "hello" --cdp-endpoint http://127.0.0.1:9224
```

If the endpoint exposes multiple inspectable windows, prefer the correct one per command:

```bash
opencli antigravity status \
--cdp-endpoint http://127.0.0.1:9224 \
--cdp-target antigravity
```

## Commands

### `opencli antigravity status`
Expand Down
116 changes: 68 additions & 48 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import chalk from 'chalk';
import { type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
import { serializeCommand, formatArgSummary } from './serialization.js';
import { render as renderOutput } from './output.js';
import { getBrowserFactory, browserSession } from './runtime.js';
import { extractBrowserEnvOverrides, getBrowserFactory, browserSession, withBrowserEnvOverrides } from './runtime.js';
import { PKG_VERSION } from './version.js';
import { printCompletionScript } from './completion.js';
import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled } from './external.js';
Expand Down Expand Up @@ -133,22 +133,26 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
.option('--wait <s>', '', '3')
.option('--auto', 'Enable interactive fuzzing')
.option('--click <labels>', 'Comma-separated labels to click before fuzzing')
.option('--cdp-endpoint <url>', 'Override the CDP endpoint for this command')
.option('--cdp-target <pattern>', 'Prefer a CDP target whose title or URL matches this pattern')
.action(async (url, opts) => {
const { exploreUrl, renderExploreSummary } = await import('./explore.js');
const clickLabels = opts.click
? opts.click.split(',').map((s: string) => s.trim())
: undefined;
const workspace = `explore:${inferHost(url, opts.site)}`;
const result = await exploreUrl(url, {
BrowserFactory: getBrowserFactory(),
site: opts.site,
goal: opts.goal,
waitSeconds: parseFloat(opts.wait),
auto: opts.auto,
clickLabels,
workspace,
await withBrowserEnvOverrides(extractBrowserEnvOverrides(opts), async () => {
const { exploreUrl, renderExploreSummary } = await import('./explore.js');
const clickLabels = opts.click
? opts.click.split(',').map((s: string) => s.trim())
: undefined;
const workspace = `explore:${inferHost(url, opts.site)}`;
const result = await exploreUrl(url, {
BrowserFactory: getBrowserFactory(),
site: opts.site,
goal: opts.goal,
waitSeconds: parseFloat(opts.wait),
auto: opts.auto,
clickLabels,
workspace,
});
console.log(renderExploreSummary(result));
});
console.log(renderExploreSummary(result));
});

program
Expand All @@ -167,18 +171,22 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
.argument('<url>')
.option('--goal <text>')
.option('--site <name>')
.option('--cdp-endpoint <url>', 'Override the CDP endpoint for this command')
.option('--cdp-target <pattern>', 'Prefer a CDP target whose title or URL matches this pattern')
.action(async (url, opts) => {
const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js');
const workspace = `generate:${inferHost(url, opts.site)}`;
const r = await generateCliFromUrl({
url,
BrowserFactory: getBrowserFactory(),
goal: opts.goal,
site: opts.site,
workspace,
await withBrowserEnvOverrides(extractBrowserEnvOverrides(opts), async () => {
const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js');
const workspace = `generate:${inferHost(url, opts.site)}`;
const r = await generateCliFromUrl({
url,
BrowserFactory: getBrowserFactory(),
goal: opts.goal,
site: opts.site,
workspace,
});
console.log(renderGenerateSummary(r));
process.exitCode = r.ok ? 0 : 1;
});
console.log(renderGenerateSummary(r));
process.exitCode = r.ok ? 0 : 1;
});

// ── Built-in: record ─────────────────────────────────────────────────────
Expand All @@ -191,37 +199,45 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
.option('--out <dir>', 'Output directory for candidates')
.option('--poll <ms>', 'Poll interval in milliseconds', '2000')
.option('--timeout <ms>', 'Auto-stop after N milliseconds (default: 60000)', '60000')
.option('--cdp-endpoint <url>', 'Override the CDP endpoint for this command')
.option('--cdp-target <pattern>', 'Prefer a CDP target whose title or URL matches this pattern')
.action(async (url, opts) => {
const { recordSession, renderRecordSummary } = await import('./record.js');
const result = await recordSession({
BrowserFactory: getBrowserFactory(),
url,
site: opts.site,
outDir: opts.out,
pollMs: parseInt(opts.poll, 10),
timeoutMs: parseInt(opts.timeout, 10),
await withBrowserEnvOverrides(extractBrowserEnvOverrides(opts), async () => {
const { recordSession, renderRecordSummary } = await import('./record.js');
const result = await recordSession({
BrowserFactory: getBrowserFactory(),
url,
site: opts.site,
outDir: opts.out,
pollMs: parseInt(opts.poll, 10),
timeoutMs: parseInt(opts.timeout, 10),
});
console.log(renderRecordSummary(result));
process.exitCode = result.candidateCount > 0 ? 0 : 1;
});
console.log(renderRecordSummary(result));
process.exitCode = result.candidateCount > 0 ? 0 : 1;
});

program
.command('cascade')
.description('Strategy cascade: find simplest working strategy')
.argument('<url>')
.option('--site <name>')
.option('--cdp-endpoint <url>', 'Override the CDP endpoint for this command')
.option('--cdp-target <pattern>', 'Prefer a CDP target whose title or URL matches this pattern')
.action(async (url, opts) => {
const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
const workspace = `cascade:${inferHost(url, opts.site)}`;
const result = await browserSession(getBrowserFactory(), async (page) => {
try {
const siteUrl = new URL(url);
await page.goto(`${siteUrl.protocol}//${siteUrl.host}`);
await page.wait(2);
} catch {}
return cascadeProbe(page, url);
}, { workspace });
console.log(renderCascadeResult(result));
await withBrowserEnvOverrides(extractBrowserEnvOverrides(opts), async () => {
const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
const workspace = `cascade:${inferHost(url, opts.site)}`;
const result = await browserSession(getBrowserFactory(), async (page) => {
try {
const siteUrl = new URL(url);
await page.goto(`${siteUrl.protocol}//${siteUrl.host}`);
await page.wait(2);
} catch {}
return cascadeProbe(page, url);
}, { workspace });
console.log(renderCascadeResult(result));
});
});

// ── Built-in: doctor / completion ──────────────────────────────────────────
Expand Down Expand Up @@ -439,9 +455,13 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
.command('serve')
.description('Start Anthropic-compatible API proxy for Antigravity')
.option('--port <port>', 'Server port (default: 8082)', '8082')
.option('--cdp-endpoint <url>', 'Override the CDP endpoint for this command')
.option('--cdp-target <pattern>', 'Prefer a CDP target whose title or URL matches this pattern')
.action(async (opts) => {
const { startServe } = await import('./clis/antigravity/serve.js');
await startServe({ port: parseInt(opts.port) });
await withBrowserEnvOverrides(extractBrowserEnvOverrides(opts), async () => {
const { startServe } = await import('./clis/antigravity/serve.js');
await startServe({ port: parseInt(opts.port) });
});
});

// ── Dynamic adapter commands ──────────────────────────────────────────────
Expand Down
17 changes: 15 additions & 2 deletions src/clis/antigravity/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,21 @@ The target Electron application MUST be launched with the remote-debugging-port
/Applications/Antigravity.app/Contents/MacOS/Electron --remote-debugging-port=9224
\`\`\`

The agent must configure the endpoint environment variable locally before invoking standard commands:
The agent can either configure the endpoint environment variable locally once:
\`\`\`bash
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
\`\`\`

If the endpoint exposes multiple inspectable targets, also set:
Or pass it per command, which is better when switching between multiple apps:
\`\`\`bash
opencli antigravity status --cdp-endpoint http://127.0.0.1:9224
\`\`\`

If the endpoint exposes multiple inspectable targets, also set or pass:
\`\`\`bash
export OPENCLI_CDP_TARGET="antigravity"
# or:
opencli antigravity status --cdp-endpoint http://127.0.0.1:9224 --cdp-target antigravity
\`\`\`

## High-Level Capabilities
Expand All @@ -39,6 +46,12 @@ opencli antigravity send "Write a python script to fetch HN top stories"
opencli antigravity extract-code > hn_fetcher.py
\`\`\`

Equivalent per-command form:
\`\`\`bash
opencli antigravity send "Write a python script to fetch HN top stories" --cdp-endpoint http://127.0.0.1:9224
opencli antigravity extract-code --cdp-endpoint http://127.0.0.1:9224 > hn_fetcher.py
\`\`\`

### Reading Real-time Logs
Agents can run long-running streaming watch instances:
\`\`\`bash
Expand Down
4 changes: 3 additions & 1 deletion src/clis/antigravity/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*
* Usage:
* OPENCLI_CDP_ENDPOINT=http://127.0.0.1:9224 opencli antigravity serve --port 8082
* opencli antigravity serve --port 8082 --cdp-endpoint http://127.0.0.1:9224 --cdp-target antigravity
* ANTHROPIC_BASE_URL=http://localhost:8082 claude
*/

Expand Down Expand Up @@ -439,7 +440,8 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
if (!endpoint) {
throw new Error(
'OPENCLI_CDP_ENDPOINT is not set.\n' +
'Usage: OPENCLI_CDP_ENDPOINT=http://127.0.0.1:9224 opencli antigravity serve'
'Usage: OPENCLI_CDP_ENDPOINT=http://127.0.0.1:9224 opencli antigravity serve\n' +
' or: opencli antigravity serve --cdp-endpoint http://127.0.0.1:9224'
);
}

Expand Down
70 changes: 70 additions & 0 deletions src/commanderAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Command } from 'commander';
import type { CliCommand } from './registry.js';

const { mockExecuteCommand, mockRender } = vi.hoisted(() => ({
mockExecuteCommand: vi.fn(),
mockRender: vi.fn(),
}));

vi.mock('./execution.js', () => ({
executeCommand: mockExecuteCommand,
}));

vi.mock('./output.js', () => ({
render: mockRender,
}));

import { registerCommandToProgram } from './commanderAdapter.js';

describe('registerCommandToProgram', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
process.exitCode = undefined;
});

it('applies command-level CDP overrides only while a browser command executes', async () => {
const seen: Array<{ endpoint?: string; target?: string }> = [];
mockExecuteCommand.mockImplementation(async () => {
seen.push({
endpoint: process.env.OPENCLI_CDP_ENDPOINT,
target: process.env.OPENCLI_CDP_TARGET,
});
return [];
});

const cmd: CliCommand = {
site: 'antigravity',
name: 'status',
description: 'status',
browser: true,
args: [],
};

const program = new Command();
const siteCmd = program.command('antigravity');
registerCommandToProgram(siteCmd, cmd);

await program.parseAsync([
'node',
'opencli',
'antigravity',
'status',
'--cdp-endpoint',
'http://127.0.0.1:9333',
'--cdp-target',
'launchpad',
]);

expect(mockExecuteCommand).toHaveBeenCalledWith(cmd, {}, false);
expect(seen).toEqual([
{
endpoint: 'http://127.0.0.1:9333',
target: 'launchpad',
},
]);
expect(process.env.OPENCLI_CDP_ENDPOINT).toBeUndefined();
expect(process.env.OPENCLI_CDP_TARGET).toBeUndefined();
});
});
10 changes: 8 additions & 2 deletions src/commanderAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { formatRegistryHelpText } from './serialization.js';
import { render as renderOutput } from './output.js';
import { executeCommand } from './execution.js';
import { CliError, ERROR_ICONS, getErrorMessage } from './errors.js';
import { extractBrowserEnvOverrides, withBrowserEnvOverrides } from './runtime.js';

/**
* Register a single CliCommand as a Commander subcommand.
Expand All @@ -43,6 +44,11 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
subCmd
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
.option('-v, --verbose', 'Debug output', false);
if (cmd.browser) {
subCmd
.option('--cdp-endpoint <url>', 'Override the CDP endpoint for this command')
.option('--cdp-target <pattern>', 'Prefer a CDP target whose title or URL matches this pattern');
}

subCmd.addHelpText('after', formatRegistryHelpText(cmd));

Expand All @@ -69,8 +75,8 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
const verbose = optionsRecord.verbose === true;
const format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
if (verbose) process.env.OPENCLI_VERBOSE = '1';

const result = await executeCommand(cmd, kwargs, verbose);
const browserEnv = extractBrowserEnvOverrides(optionsRecord);
const result = await withBrowserEnvOverrides(browserEnv, async () => executeCommand(cmd, kwargs, verbose));

if (verbose && (!result || (Array.isArray(result) && result.length === 0))) {
console.error(chalk.yellow('[Verbose] Warning: Command returned an empty result.'));
Expand Down
Loading