feat(antigravity): support cli all electron app via CDP
This commit is contained in:
1 parent
bde1c53a3e
commit
7e776e2bd5
11 files changed
+422
No files matched your search
@@ -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.
|
||||
@@ -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 中止。
|
||||
@@ -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
|
||||
\`\`\`
|
||||
@@ -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 }];
|
||||
},
|
||||
});
|
||||
@@ -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 }));
|
||||
},
|
||||
});
|
||||
@@ -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}` }];
|
||||
},
|
||||
});
|
||||
@@ -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' }];
|
||||
},
|
||||
});
|
||||
@@ -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
|
||||
}];
|
||||
},
|
||||
});
|
||||
@@ -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 }];
|
||||
},
|
||||
});
|
||||
@@ -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'),
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user