feat(antigravity): support cli all electron app via CDP

This commit is contained in:
jackwener committed 2026-03-18 16:44:36 +08:00
1 parent bde1c53a3e
commit 7e776e2bd5
11 files changed
+422

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
# Antigravity CLI Adapter
🔥 **CLI All Electron Apps! The Most Powerful Update Has Arrived!** 🔥
Turn ANY Electron application into a CLI tool! Recombine, script, and extend applications like Antigravity Ultra seamlessly. Now AI can control itself natively. Unlimited possibilities await!
Turn your local Antigravity desktop application into a programmable AI node via Chrome DevTools Protocol (CDP). This allows you to compose complex LLM workflows entirely through the terminal by manipulating the actual UI natively, bypassing any API restrictions.
## Prerequisites
Start the Antigravity desktop app with the Chrome DevTools `remote-debugging-port` flag:
\`\`\`bash
# Start Antigravity in the background
/Applications/Antigravity.app/Contents/MacOS/Electron \\
--remote-debugging-port=9224 \\
--remote-allow-origins="*"
\`\`\`
*(Note: Depending on your installation, the executable might be named differently, e.g., \`Antigravity\` instead of \`Electron\`.)*
Next, set the target port in your terminal session to tell OpenCLI where to connect:
\`\`\`bash
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
\`\`\`
## Available Commands
### \`opencli antigravity status\`
Check the Chromium CDP connection. Returns the current window title and active internal URL.
### \`opencli antigravity send <message>\`
Send a text prompt to the AI. Automatically locates the Lexical editor input box, types the prompt securely, and hits Enter.
### \`opencli antigravity read\`
Scrape the entire current conversation history block as pure text. Useful for feeding the context to another script.
### \`opencli antigravity new\`
Click the "New Conversation" button to instantly clear the UI state and start fresh.
### \`opencli antigravity extract-code\`
Extract any multi-line code blocks from the current conversation view. Ideal for automated script extraction (e.g. \`opencli antigravity extract-code > script.sh\`).
### \`opencli antigravity model <name>\`
Quickly target and switch the active LLM engine. Example: \`opencli antigravity model claude\` or \`opencli antigravity model gemini\`.
### \`opencli antigravity watch\`
A long-running, streaming process that continuously polls the Antigravity UI for chat updates and outputs them in real-time to standard output.
+52
View File
@@ -0,0 +1,52 @@
# Antigravity CLI Adapter (探针插件)
🔥 **opencli 支持 CLI 化所有 electron 应用!最强大更新来袭!** 🔥
CLI all electron!现在支持把所有 electron 应用 CLI 化,从而组合出各种神奇的能力。
如果你在使用诸如 Antigravity Ultra 等工具时觉得不够灵活或难以扩展,现在通过 OpenCLI 把他 CLI 化,轻松打破界限。
现在,**AI 可以自己控制自己**!结合 cc/openclaw 就可以远程控制任何 electron 应用!无限玩法!!
通过 Chrome DevTools Protocol (CDP),将你本地运行的 Antigravity 桌面客户端转变为一个完全可编程的 AI 节点。这让你可以在命令行终端中直接操控它的 UI 界面,实现真正的“零 API 限制”本地自动化大模型工作流调度。
## 开发准备
首先,**请在终端启动 Antigravity 桌面版**,并附加上允许远程调试(CDP)的内核启动参数:
\`\`\`bash
# 在后台启动并驻留
/Applications/Antigravity.app/Contents/MacOS/Electron \\
--remote-debugging-port=9224 \\
--remote-allow-origins="*"
\`\`\`
*(注意:如果你打包的应用重命名过主构建,可能需要把 `Electron` 换成实际的可执行文件名,如 `Antigravity`)*
接下来,在你想执行 CLI 命令的另一个新终端板块里,声明要连入的本地调试端口环境变量:
\`\`\`bash
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
\`\`\`
## 全部指令一览
### \`opencli antigravity status\`
快速检查当前探针与内核 CDP 的连接状态。会返回底层的当前 URL 和网页 Title。
### \`opencli antigravity send <message>\`
给 Agent 发送消息。它会自动定位到底部的 Lexical 输入框,安全地注入你的指定文本然后模拟回车发送。
### \`opencli antigravity read\`
全量抓取当前的对话面板,将所有历史聊天记录作为一整块纯文本取回。
### \`opencli antigravity new\`
模拟点击侧边栏顶部的“开启新对话”按钮,瞬间清空并重置 Agent 的上下文状态。
### \`opencli antigravity extract-code\`
从当前的 Agent 聊天记录中单独提取所有的多行代码块。非常适合自动化脚手架开发(例如直接重定向输出写入本地文件:\`opencli antigravity extract-code > script.sh\`)。
### \`opencli antigravity model <name>\`
切换大模型引擎。只需传入关键词(比如:\`opencli antigravity model claude\` 或 \`model gemini\`),它会自动帮你点开模型选择菜单并模拟点击。
### \`opencli antigravity watch\`
开启一个长连接流式监听。通过持续轮询 DOM 的变化量,它能像流式 API 一样,在终端实时向你推送 Agent 刚刚打出的那一行最新回复,直到你按 Ctrl+C 中止。
+42
View File
@@ -0,0 +1,42 @@
---
description: How to automate Antigravity using OpenCLI
---
# Antigravity Automation Skill
This skill allows AI agents to control the [Antigravity](https://github.com/chengazhen/Antigravity) desktop app (and any Electron app with CDP enabled) programmatically via OpenCLI.
## Requirements
The target Electron application MUST be launched with the remote-debugging-port flag:
\`\`\`bash
/Applications/Antigravity.app/Contents/MacOS/Electron --remote-debugging-port=9224 --remote-allow-origins="*"
\`\`\`
The agent must configure the endpoint environment variable locally before invoking standard commands:
\`\`\`bash
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
\`\`\`
## High-Level Capabilities
1. **Send Messages (`opencli antigravity send <message>`)**: Type and send a message directly into the chat UI.
2. **Read History (`opencli antigravity read`)**: Scrape the raw chat transcript from the main UI container.
3. **Extract Code (`opencli antigravity extract-code`)**: Automatically isolate and extract source code text blocks from the AI's recent answers.
4. **Switch Models (`opencli antigravity model <name>`)**: Instantly toggle the active LLM (e.g., \`gemini\`, \`claude\`).
5. **Clear Context (`opencli antigravity new`)**: Start a fresh conversation.
## Examples for Automated Workflows
### Generating and Saving Code
\`\`\`bash
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
opencli antigravity send "Write a python script to fetch HN top stories"
# wait ~10-15 seconds for output to render
opencli antigravity extract-code > hn_fetcher.py
\`\`\`
### Reading Real-time Logs
Agents can run long-running streaming watch instances:
\`\`\`bash
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
opencli antigravity watch
\`\`\`
+30
View File
@@ -0,0 +1,30 @@
import { cli, Strategy } from '../../registry.js';
import * as fs from 'node:fs';
export const dumpCommand = cli({
site: 'antigravity',
name: 'dump',
description: 'Dump the DOM to help AI understand the UI',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [],
columns: ['htmlFile', 'snapFile'],
func: async (page) => {
// Extract HTML
const html = await page.evaluate('document.body.innerHTML');
fs.writeFileSync('/tmp/antigravity-dom.html', html);
// Extract Snapshot
let snapFile = '';
try {
const snap = await page.snapshot({ raw: true });
snapFile = '/tmp/antigravity-snapshot.json';
fs.writeFileSync(snapFile, JSON.stringify(snap, null, 2));
} catch (e) {
snapFile = 'Failed';
}
return [{ htmlFile: '/tmp/antigravity-dom.html', snapFile }];
},
});
+34
View File
@@ -0,0 +1,34 @@
import { cli, Strategy } from '../../registry.js';
export const extractCodeCommand = cli({
site: 'antigravity',
name: 'extract-code',
description: 'Extract multi-line code blocks from the current Antigravity conversation',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [],
columns: ['code'],
func: async (page) => {
const blocks = await page.evaluate(`
async () => {
// Find standard pre/code blocks
let elements = Array.from(document.querySelectorAll('pre code'));
// Fallback to Monaco editor content inside the UI
if (elements.length === 0) {
elements = Array.from(document.querySelectorAll('.monaco-editor'));
}
// Generic fallback to any code tag that spans multiple lines
if (elements.length === 0) {
elements = Array.from(document.querySelectorAll('code')).filter(c => c.innerText.includes('\\n'));
}
return elements.map(el => el.innerText).filter(text => text.trim().length > 0);
}
`);
return blocks.map((code: string) => ({ code }));
},
});
+47
View File
@@ -0,0 +1,47 @@
import { cli, Strategy } from '../../registry.js';
export const modelCommand = cli({
site: 'antigravity',
name: 'model',
description: 'Switch the active LLM model in Antigravity',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [
{ name: 'name', help: 'Target model name (e.g. claude, gemini, o1)', required: true, positional: true }
],
columns: ['status'],
func: async (page, kwargs) => {
const targetName = kwargs.name.toLowerCase();
await page.evaluate(`
async () => {
const targetModelName = ${JSON.stringify(targetName)};
// 1. Locate the model selector dropdown trigger
const trigger = document.querySelector('div[aria-haspopup="dialog"] > div[tabindex="0"]');
if (!trigger) throw new Error('Could not find the model selector trigger in the UI');
trigger.click();
// 2. Wait a brief moment for React to mount the Portal/Dialog
await new Promise(r => setTimeout(r, 200));
// 3. Find the option spanning target text
const spans = Array.from(document.querySelectorAll('[role="dialog"] span'));
const target = spans.find(s => s.innerText.toLowerCase().includes(targetModelName));
if (!target) {
// If not found, click the trigger again to close it safely
trigger.click();
throw new Error('Model matching "' + targetModelName + '" was not found in the dropdown list.');
}
// 4. Click the closest parent that handles the row action
const optionNode = target.closest('.cursor-pointer') || target;
optionNode.click();
}
`);
await page.wait(0.5);
return [{ status: `Model switched to: ${kwargs.name}` }];
},
});
+28
View File
@@ -0,0 +1,28 @@
import { cli, Strategy } from '../../registry.js';
export const newCommand = cli({
site: 'antigravity',
name: 'new',
description: 'Start a new conversation / clear context in Antigravity',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [],
columns: ['status'],
func: async (page) => {
await page.evaluate(`
async () => {
const btn = document.querySelector('[data-tooltip-id="new-conversation-tooltip"]');
if (!btn) throw new Error('Could not find New Conversation button');
// In case it's disabled, we must check, but we'll try to click it anyway
btn.click();
}
`);
// Give it a moment to reset the UI
await page.wait(0.5);
return [{ status: 'Successfully started a new conversation' }];
},
});
+36
View File
@@ -0,0 +1,36 @@
import { cli, Strategy } from '../../registry.js';
export const readCommand = cli({
site: 'antigravity',
name: 'read',
description: 'Read the latest chat messages from Antigravity AI',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [
{ name: 'last', help: 'Number of recent messages to read (not fully implemented due to generic structure, currently returns full history text or latest chunk)' }
],
columns: ['role', 'content'],
func: async (page, kwargs) => {
// We execute a script inside Antigravity's Chromium environment to extract the text
// of the entire conversation pane.
const rawText = await page.evaluate(`
async () => {
const container = document.getElementById('conversation');
if (!container) throw new Error('Could not find conversation container');
// Extract the full visible text of the conversation
// In Electron/Chromium, innerText preserves basic visual line breaks nicely
return container.innerText;
}
`);
// We can do simple heuristic parsing based on typical visual markers if needed.
// For now, we return the entire text blob, or just the last 2000 characters if it's too long.
const cleanText = String(rawText).trim();
return [{
role: 'history',
content: cleanText
}];
},
});
+40
View File
@@ -0,0 +1,40 @@
import { cli, Strategy } from '../../registry.js';
export const sendCommand = cli({
site: 'antigravity',
name: 'send',
description: 'Send a message to Antigravity AI via the internal Lexical editor',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [
{ name: 'message', help: 'The message text to send', required: true, positional: true }
],
columns: ['status', 'message'],
func: async (page, kwargs) => {
const text = kwargs.message;
// We use evaluate to focus and insert text because Lexical editors maintain
// absolute control over their DOM and don't respond to raw node.textContent.
// document.execCommand simulates a native paste/typing action perfectly.
await page.evaluate(`
async () => {
const container = document.getElementById('antigravity.agentSidePanelInputBox');
if (!container) throw new Error('Could not find antigravity.agentSidePanelInputBox');
const editor = container.querySelector('[data-lexical-editor="true"]');
if (!editor) throw new Error('Could not find Antigravity input box');
editor.focus();
document.execCommand('insertText', false, ${JSON.stringify(text)});
}
`);
// Wait for the React/Lexical state to flush the new input
await page.wait(0.5);
// Press Enter to submit the message
await page.pressKey('Enter');
return [{ status: 'Sent successfully', message: text }];
},
});
+19
View File
@@ -0,0 +1,19 @@
import { cli, Strategy } from '../../registry.js';
export const statusCommand = cli({
site: 'antigravity',
name: 'status',
description: 'Check Antigravity CDP connection and get current page state',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [],
columns: ['status', 'url', 'title'],
func: async (page) => {
return {
status: 'Connected',
url: await page.evaluate('window.location.href'),
title: await page.evaluate('document.title'),
};
},
});
+45
View File
@@ -0,0 +1,45 @@
import { cli, Strategy } from '../../registry.js';
export const watchCommand = cli({
site: 'antigravity',
name: 'watch',
description: 'Stream new chat messages from Antigravity in real-time',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [],
timeoutSeconds: 86400, // Run for up to 24 hours
columns: [], // We use direct stdout streaming
func: async (page) => {
console.log('Watching Antigravity chat... (Press Ctrl+C to stop)');
let lastLength = 0;
// Loop until process gets killed
while (true) {
const text = await page.evaluate(`
async () => {
const container = document.getElementById('conversation');
return container ? container.innerText : '';
}
`);
const currentLength = text.length;
if (currentLength > lastLength) {
// Delta mode
const newSegment = text.substring(lastLength);
if (newSegment.trim().length > 0) {
process.stdout.write(newSegment);
}
lastLength = currentLength;
} else if (currentLength < lastLength) {
// The conversation was cleared or updated significantly
lastLength = currentLength;
console.log('\\n--- Conversation Cleared/Changed ---\\n');
process.stdout.write(text);
}
await new Promise(resolve => setTimeout(resolve, 500));
}
},
});