diff --git a/src/clis/antigravity/README.md b/src/clis/antigravity/README.md new file mode 100644 index 0000000..c63f05d --- /dev/null +++ b/src/clis/antigravity/README.md @@ -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 \` +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 \` +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. diff --git a/src/clis/antigravity/README.zh-CN.md b/src/clis/antigravity/README.zh-CN.md new file mode 100644 index 0000000..10dbf18 --- /dev/null +++ b/src/clis/antigravity/README.zh-CN.md @@ -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 \` +给 Agent 发送消息。它会自动定位到底部的 Lexical 输入框,安全地注入你的指定文本然后模拟回车发送。 + +### \`opencli antigravity read\` +全量抓取当前的对话面板,将所有历史聊天记录作为一整块纯文本取回。 + +### \`opencli antigravity new\` +模拟点击侧边栏顶部的“开启新对话”按钮,瞬间清空并重置 Agent 的上下文状态。 + +### \`opencli antigravity extract-code\` +从当前的 Agent 聊天记录中单独提取所有的多行代码块。非常适合自动化脚手架开发(例如直接重定向输出写入本地文件:\`opencli antigravity extract-code > script.sh\`)。 + +### \`opencli antigravity model \` +切换大模型引擎。只需传入关键词(比如:\`opencli antigravity model claude\` 或 \`model gemini\`),它会自动帮你点开模型选择菜单并模拟点击。 + +### \`opencli antigravity watch\` +开启一个长连接流式监听。通过持续轮询 DOM 的变化量,它能像流式 API 一样,在终端实时向你推送 Agent 刚刚打出的那一行最新回复,直到你按 Ctrl+C 中止。 diff --git a/src/clis/antigravity/SKILL.md b/src/clis/antigravity/SKILL.md new file mode 100644 index 0000000..1f6f7b4 --- /dev/null +++ b/src/clis/antigravity/SKILL.md @@ -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 `)**: 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 `)**: 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 +\`\`\` diff --git a/src/clis/antigravity/dump.ts b/src/clis/antigravity/dump.ts new file mode 100644 index 0000000..1fa4ca3 --- /dev/null +++ b/src/clis/antigravity/dump.ts @@ -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 }]; + }, +}); diff --git a/src/clis/antigravity/extract-code.ts b/src/clis/antigravity/extract-code.ts new file mode 100644 index 0000000..a69dcb4 --- /dev/null +++ b/src/clis/antigravity/extract-code.ts @@ -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 })); + }, +}); diff --git a/src/clis/antigravity/model.ts b/src/clis/antigravity/model.ts new file mode 100644 index 0000000..ea378ac --- /dev/null +++ b/src/clis/antigravity/model.ts @@ -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}` }]; + }, +}); diff --git a/src/clis/antigravity/new.ts b/src/clis/antigravity/new.ts new file mode 100644 index 0000000..ab13bef --- /dev/null +++ b/src/clis/antigravity/new.ts @@ -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' }]; + }, +}); diff --git a/src/clis/antigravity/read.ts b/src/clis/antigravity/read.ts new file mode 100644 index 0000000..46a0cff --- /dev/null +++ b/src/clis/antigravity/read.ts @@ -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 + }]; + }, +}); diff --git a/src/clis/antigravity/send.ts b/src/clis/antigravity/send.ts new file mode 100644 index 0000000..992c860 --- /dev/null +++ b/src/clis/antigravity/send.ts @@ -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 }]; + }, +}); diff --git a/src/clis/antigravity/status.ts b/src/clis/antigravity/status.ts new file mode 100644 index 0000000..5ae177b --- /dev/null +++ b/src/clis/antigravity/status.ts @@ -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'), + }; + }, +}); diff --git a/src/clis/antigravity/watch.ts b/src/clis/antigravity/watch.ts new file mode 100644 index 0000000..8183a20 --- /dev/null +++ b/src/clis/antigravity/watch.ts @@ -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)); + } + }, +});