@jackwener/opencli 0.7.11 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CDP.md +103 -0
  2. package/CDP.zh-CN.md +103 -0
  3. package/README.md +5 -0
  4. package/README.zh-CN.md +5 -0
  5. package/dist/browser/discover.d.ts +15 -0
  6. package/dist/browser/discover.js +68 -2
  7. package/dist/browser/errors.d.ts +2 -1
  8. package/dist/browser/errors.js +13 -0
  9. package/dist/browser/index.d.ts +1 -0
  10. package/dist/browser/index.js +1 -0
  11. package/dist/browser/mcp.js +8 -3
  12. package/dist/browser/page.js +11 -2
  13. package/dist/cli-manifest.json +246 -0
  14. package/dist/clis/antigravity/dump.d.ts +1 -0
  15. package/dist/clis/antigravity/dump.js +28 -0
  16. package/dist/clis/antigravity/extract-code.d.ts +1 -0
  17. package/dist/clis/antigravity/extract-code.js +32 -0
  18. package/dist/clis/antigravity/model.d.ts +1 -0
  19. package/dist/clis/antigravity/model.js +44 -0
  20. package/dist/clis/antigravity/new.d.ts +1 -0
  21. package/dist/clis/antigravity/new.js +25 -0
  22. package/dist/clis/antigravity/read.d.ts +1 -0
  23. package/dist/clis/antigravity/read.js +34 -0
  24. package/dist/clis/antigravity/send.d.ts +1 -0
  25. package/dist/clis/antigravity/send.js +35 -0
  26. package/dist/clis/antigravity/status.d.ts +1 -0
  27. package/dist/clis/antigravity/status.js +18 -0
  28. package/dist/clis/antigravity/watch.d.ts +1 -0
  29. package/dist/clis/antigravity/watch.js +41 -0
  30. package/dist/clis/barchart/flow.js +56 -58
  31. package/dist/clis/xiaoyuzhou/episode.d.ts +1 -0
  32. package/dist/clis/xiaoyuzhou/episode.js +28 -0
  33. package/dist/clis/xiaoyuzhou/podcast-episodes.d.ts +1 -0
  34. package/dist/clis/xiaoyuzhou/podcast-episodes.js +36 -0
  35. package/dist/clis/xiaoyuzhou/podcast.d.ts +1 -0
  36. package/dist/clis/xiaoyuzhou/podcast.js +27 -0
  37. package/dist/clis/xiaoyuzhou/utils.d.ts +16 -0
  38. package/dist/clis/xiaoyuzhou/utils.js +55 -0
  39. package/dist/clis/xiaoyuzhou/utils.test.d.ts +1 -0
  40. package/dist/clis/xiaoyuzhou/utils.test.js +99 -0
  41. package/dist/doctor.js +8 -0
  42. package/dist/engine.d.ts +1 -1
  43. package/dist/engine.js +59 -1
  44. package/dist/main.js +2 -15
  45. package/dist/pipeline/executor.js +2 -24
  46. package/dist/pipeline/registry.d.ts +19 -0
  47. package/dist/pipeline/registry.js +41 -0
  48. package/package.json +1 -1
  49. package/src/browser/discover.ts +79 -5
  50. package/src/browser/errors.ts +17 -1
  51. package/src/browser/index.ts +1 -0
  52. package/src/browser/mcp.ts +8 -3
  53. package/src/browser/page.ts +21 -2
  54. package/src/clis/antigravity/README.md +49 -0
  55. package/src/clis/antigravity/README.zh-CN.md +52 -0
  56. package/src/clis/antigravity/SKILL.md +42 -0
  57. package/src/clis/antigravity/dump.ts +30 -0
  58. package/src/clis/antigravity/extract-code.ts +34 -0
  59. package/src/clis/antigravity/model.ts +47 -0
  60. package/src/clis/antigravity/new.ts +28 -0
  61. package/src/clis/antigravity/read.ts +36 -0
  62. package/src/clis/antigravity/send.ts +40 -0
  63. package/src/clis/antigravity/status.ts +19 -0
  64. package/src/clis/antigravity/watch.ts +45 -0
  65. package/src/clis/barchart/flow.ts +57 -58
  66. package/src/clis/xiaoyuzhou/episode.ts +28 -0
  67. package/src/clis/xiaoyuzhou/podcast-episodes.ts +36 -0
  68. package/src/clis/xiaoyuzhou/podcast.ts +27 -0
  69. package/src/clis/xiaoyuzhou/utils.test.ts +122 -0
  70. package/src/clis/xiaoyuzhou/utils.ts +65 -0
  71. package/src/doctor.ts +9 -0
  72. package/src/engine.ts +58 -1
  73. package/src/main.ts +6 -11
  74. package/src/pipeline/executor.ts +2 -28
  75. package/src/pipeline/registry.ts +60 -0
  76. package/tests/e2e/public-commands.test.ts +62 -0
@@ -111,13 +111,87 @@ export function findMcpServerPath(): string | null {
111
111
  return _cachedMcpServerPath;
112
112
  }
113
113
 
114
- function buildRuntimeArgs(input?: { executablePath?: string | null }): string[] {
114
+ /**
115
+ * Chrome 144+ auto-discovery: read DevToolsActivePort file to get CDP endpoint.
116
+ *
117
+ * Starting with Chrome 144, users can enable remote debugging from
118
+ * chrome://inspect#remote-debugging without any command-line flags.
119
+ * Chrome writes the active port and browser GUID to a DevToolsActivePort file
120
+ * in the user data directory, which we read to construct the WebSocket endpoint.
121
+ */
122
+ export function discoverChromeEndpoint(): string | null {
123
+ const candidates: string[] = [];
124
+
125
+ // User-specified Chrome data dir takes highest priority
126
+ if (process.env.CHROME_USER_DATA_DIR) {
127
+ candidates.push(path.join(process.env.CHROME_USER_DATA_DIR, 'DevToolsActivePort'));
128
+ }
129
+
130
+ // Standard Chrome/Edge user data dirs per platform
131
+ if (process.platform === 'win32') {
132
+ const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
133
+ candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'));
134
+ candidates.push(path.join(localAppData, 'Microsoft', 'Edge', 'User Data', 'DevToolsActivePort'));
135
+ } else if (process.platform === 'darwin') {
136
+ candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort'));
137
+ candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge', 'DevToolsActivePort'));
138
+ } else {
139
+ candidates.push(path.join(os.homedir(), '.config', 'google-chrome', 'DevToolsActivePort'));
140
+ candidates.push(path.join(os.homedir(), '.config', 'chromium', 'DevToolsActivePort'));
141
+ candidates.push(path.join(os.homedir(), '.config', 'microsoft-edge', 'DevToolsActivePort'));
142
+ }
143
+
144
+ for (const filePath of candidates) {
145
+ try {
146
+ const content = fs.readFileSync(filePath, 'utf-8').trim();
147
+ const lines = content.split('\n');
148
+ if (lines.length >= 2) {
149
+ const port = parseInt(lines[0], 10);
150
+ const browserPath = lines[1]; // e.g. /devtools/browser/<GUID>
151
+ if (port > 0 && browserPath.startsWith('/devtools/browser/')) {
152
+ return `ws://127.0.0.1:${port}${browserPath}`;
153
+ }
154
+ }
155
+ } catch {}
156
+ }
157
+ return null;
158
+ }
159
+
160
+ export function resolveCdpEndpoint(): { endpoint?: string; requestedCdp: boolean } {
161
+ const envVal = process.env.OPENCLI_CDP_ENDPOINT;
162
+ if (envVal === '1' || envVal?.toLowerCase() === 'true') {
163
+ const autoDiscovered = discoverChromeEndpoint();
164
+ return { endpoint: autoDiscovered ?? envVal, requestedCdp: true };
165
+ }
166
+
167
+ if (envVal) {
168
+ return { endpoint: envVal, requestedCdp: true };
169
+ }
170
+
171
+ // Fallback to auto-discovery if not explicitly set
172
+ const autoDiscovered = discoverChromeEndpoint();
173
+ if (autoDiscovered) {
174
+ return { endpoint: autoDiscovered, requestedCdp: true };
175
+ }
176
+
177
+ return { requestedCdp: false };
178
+ }
179
+
180
+ function buildRuntimeArgs(input?: { executablePath?: string | null; cdpEndpoint?: string }): string[] {
115
181
  const args: string[] = [];
182
+
183
+ // Priority 1: CDP endpoint (remote Chrome debugging or local Auto-Discovery)
184
+ if (input?.cdpEndpoint) {
185
+ args.push('--cdp-endpoint', input.cdpEndpoint);
186
+ return args;
187
+ }
188
+
189
+ // Priority 2: Extension mode (local Chrome with MCP Bridge extension)
116
190
  if (!process.env.CI) {
117
- // Local: always connect to user's running Chrome via MCP Bridge extension
118
191
  args.push('--extension');
119
192
  }
120
- // CI: standalone mode — @playwright/mcp launches its own browser (headed by default).
193
+
194
+ // CI/standalone mode: @playwright/mcp launches its own browser (headed by default).
121
195
  // xvfb provides a virtual display for headed mode in GitHub Actions.
122
196
  if (input?.executablePath) {
123
197
  args.push('--executable-path', input.executablePath);
@@ -125,11 +199,11 @@ function buildRuntimeArgs(input?: { executablePath?: string | null }): string[]
125
199
  return args;
126
200
  }
127
201
 
128
- export function buildMcpArgs(input: { mcpPath: string; executablePath?: string | null }): string[] {
202
+ export function buildMcpArgs(input: { mcpPath: string; executablePath?: string | null; cdpEndpoint?: string }): string[] {
129
203
  return [input.mcpPath, ...buildRuntimeArgs(input)];
130
204
  }
131
205
 
132
- export function buildMcpLaunchSpec(input: { mcpPath?: string | null; executablePath?: string | null }): {
206
+ export function buildMcpLaunchSpec(input: { mcpPath?: string | null; executablePath?: string | null; cdpEndpoint?: string }): {
133
207
  command: string;
134
208
  args: string[];
135
209
  usedNpxFallback: boolean;
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { createHash } from 'node:crypto';
6
6
 
7
- export type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'unknown';
7
+ export type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'cdp-connection-failed' | 'unknown';
8
8
 
9
9
  export type ConnectFailureInput = {
10
10
  kind: ConnectFailureKind;
@@ -26,6 +26,15 @@ export function formatBrowserConnectError(input: ConnectFailureInput): Error {
26
26
  const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : '';
27
27
  const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : '';
28
28
 
29
+ if (input.kind === 'cdp-connection-failed') {
30
+ return new Error(
31
+ `Failed to connect to remote Chrome via CDP endpoint.\n\n` +
32
+ `Check if Chrome is running with remote debugging enabled (--remote-debugging-port=9222) or DevToolsActivePort is available under chrome://inspect#remote-debugging.\n` +
33
+ `If you specified OPENCLI_CDP_ENDPOINT=1, auto-discovery might have failed.` +
34
+ suffix,
35
+ );
36
+ }
37
+
29
38
  if (input.kind === 'missing-token') {
30
39
  return new Error(
31
40
  'Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
@@ -74,9 +83,16 @@ export function inferConnectFailureKind(args: {
74
83
  stderr: string;
75
84
  rawMessage?: string;
76
85
  exited?: boolean;
86
+ isCdpMode?: boolean;
77
87
  }): ConnectFailureKind {
78
88
  const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase();
79
89
 
90
+ if (args.isCdpMode) {
91
+ if (args.rawMessage?.startsWith('MCP init failed:')) return 'mcp-init';
92
+ if (args.exited) return 'cdp-connection-failed';
93
+ return 'cdp-connection-failed';
94
+ }
95
+
80
96
  if (!args.hasExtensionToken)
81
97
  return 'missing-token';
82
98
  if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge'))
@@ -9,6 +9,7 @@ export { Page } from './page.js';
9
9
  export { PlaywrightMCP } from './mcp.js';
10
10
  export { getTokenFingerprint, formatBrowserConnectError } from './errors.js';
11
11
  export type { ConnectFailureKind, ConnectFailureInput } from './errors.js';
12
+ export { resolveCdpEndpoint } from './discover.js';
12
13
 
13
14
  // Test-only helpers — exposed for unit tests
14
15
  import { createJsonRpcRequest } from './mcp.js';
@@ -9,7 +9,7 @@ import { withTimeoutMs, DEFAULT_BROWSER_CONNECT_TIMEOUT } from '../runtime.js';
9
9
  import { PKG_VERSION } from '../version.js';
10
10
  import { Page } from './page.js';
11
11
  import { getTokenFingerprint, formatBrowserConnectError, inferConnectFailureKind } from './errors.js';
12
- import { findMcpServerPath, buildMcpLaunchSpec } from './discover.js';
12
+ import { findMcpServerPath, buildMcpLaunchSpec, resolveCdpEndpoint } from './discover.js';
13
13
  import { extractTabIdentities, extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
14
14
 
15
15
  const STDERR_BUFFER_LIMIT = 16 * 1024;
@@ -114,7 +114,8 @@ export class PlaywrightMCP {
114
114
  return new Promise<Page>((resolve, reject) => {
115
115
  const isDebug = process.env.DEBUG?.includes('opencli:mcp');
116
116
  const debugLog = (msg: string) => isDebug && console.error(`[opencli:mcp] ${msg}`);
117
- const useExtension = !!process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
117
+ const { endpoint: cdpEndpoint, requestedCdp } = resolveCdpEndpoint();
118
+ const useExtension = !requestedCdp;
118
119
  const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
119
120
  const tokenFingerprint = getTokenFingerprint(extensionToken);
120
121
  let stderrBuffer = '';
@@ -150,15 +151,17 @@ export class PlaywrightMCP {
150
151
  settleError(inferConnectFailureKind({
151
152
  hasExtensionToken: !!extensionToken,
152
153
  stderr: stderrBuffer,
154
+ isCdpMode: requestedCdp,
153
155
  }));
154
156
  }, timeout * 1000);
155
157
 
156
158
  const launchSpec = buildMcpLaunchSpec({
157
159
  mcpPath,
158
160
  executablePath: process.env.OPENCLI_BROWSER_EXECUTABLE_PATH,
161
+ cdpEndpoint,
159
162
  });
160
163
  if (process.env.OPENCLI_VERBOSE) {
161
- console.error(`[opencli] Mode: ${useExtension ? 'extension' : 'standalone'}`);
164
+ console.error(`[opencli] Mode: ${requestedCdp ? 'CDP' : useExtension ? 'extension' : 'standalone'}`);
162
165
  if (useExtension) console.error(`[opencli] Extension token: fingerprint ${tokenFingerprint}`);
163
166
  if (launchSpec.usedNpxFallback) {
164
167
  console.error('[opencli] Playwright MCP not found locally; bootstrapping via npx @playwright/mcp@latest');
@@ -218,6 +221,7 @@ export class PlaywrightMCP {
218
221
  hasExtensionToken: !!extensionToken,
219
222
  stderr: stderrBuffer,
220
223
  exited: true,
224
+ isCdpMode: requestedCdp,
221
225
  }), { exitCode: code });
222
226
  }
223
227
  });
@@ -235,6 +239,7 @@ export class PlaywrightMCP {
235
239
  hasExtensionToken: !!extensionToken,
236
240
  stderr: stderrBuffer,
237
241
  rawMessage: `MCP init failed: ${resp.error.message}`,
242
+ isCdpMode: requestedCdp,
238
243
  }), { rawMessage: resp.error.message });
239
244
  return;
240
245
  }
@@ -6,6 +6,7 @@ import { formatSnapshot } from '../snapshotFormatter.js';
6
6
  import { normalizeEvaluateSource } from '../pipeline/template.js';
7
7
  import { generateInterceptorJs, generateReadInterceptedJs } from '../interceptor.js';
8
8
  import type { IPage } from '../types.js';
9
+ import { BrowserConnectError } from '../errors.js';
9
10
 
10
11
  /**
11
12
  * Page abstraction wrapping JSON-RPC calls to Playwright MCP.
@@ -18,10 +19,28 @@ export class Page implements IPage {
18
19
  if (resp.error) throw new Error(`page.${method}: ${(resp.error as any).message ?? JSON.stringify(resp.error)}`);
19
20
  // Extract text content from MCP result
20
21
  const result = resp.result as any;
22
+
23
+ if (result?.isError) {
24
+ const errorText = result.content?.find((c: any) => c.type === 'text')?.text || 'Unknown MCP Error';
25
+ throw new BrowserConnectError(
26
+ errorText,
27
+ 'Please check if the browser is running or if the Playwright MCP / CDP connection is configured correctly.'
28
+ );
29
+ }
30
+
21
31
  if (result?.content) {
22
32
  const textParts = result.content.filter((c: any) => c.type === 'text');
23
- if (textParts.length === 1) {
24
- let text = textParts[0].text;
33
+ if (textParts.length >= 1) {
34
+ let text = textParts[textParts.length - 1].text; // Usually the main output is in the last text block
35
+
36
+ // Some versions of the MCP return error text without the `isError` boolean flag
37
+ if (typeof text === 'string' && text.trim().startsWith('### Error')) {
38
+ throw new BrowserConnectError(
39
+ text.trim(),
40
+ 'Please check if the browser is running or if the Playwright MCP / CDP connection is configured correctly.'
41
+ );
42
+ }
43
+
25
44
  // MCP browser_evaluate returns: "[JSON]\n### Ran Playwright code\n```js\n...\n```"
26
45
  // Strip the "### Ran Playwright code" suffix to get clean JSON
27
46
  const codeMarker = text.indexOf('### Ran Playwright code');
@@ -0,0 +1,49 @@
1
+ # Antigravity CLI Adapter
2
+
3
+ 🔥 **CLI All Electron Apps! The Most Powerful Update Has Arrived!** 🔥
4
+
5
+ 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!
6
+
7
+ 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.
8
+
9
+ ## Prerequisites
10
+
11
+ Start the Antigravity desktop app with the Chrome DevTools `remote-debugging-port` flag:
12
+
13
+ \`\`\`bash
14
+ # Start Antigravity in the background
15
+ /Applications/Antigravity.app/Contents/MacOS/Electron \\
16
+ --remote-debugging-port=9224 \\
17
+ --remote-allow-origins="*"
18
+ \`\`\`
19
+
20
+ *(Note: Depending on your installation, the executable might be named differently, e.g., \`Antigravity\` instead of \`Electron\`.)*
21
+
22
+ Next, set the target port in your terminal session to tell OpenCLI where to connect:
23
+
24
+ \`\`\`bash
25
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
26
+ \`\`\`
27
+
28
+ ## Available Commands
29
+
30
+ ### \`opencli antigravity status\`
31
+ Check the Chromium CDP connection. Returns the current window title and active internal URL.
32
+
33
+ ### \`opencli antigravity send <message>\`
34
+ Send a text prompt to the AI. Automatically locates the Lexical editor input box, types the prompt securely, and hits Enter.
35
+
36
+ ### \`opencli antigravity read\`
37
+ Scrape the entire current conversation history block as pure text. Useful for feeding the context to another script.
38
+
39
+ ### \`opencli antigravity new\`
40
+ Click the "New Conversation" button to instantly clear the UI state and start fresh.
41
+
42
+ ### \`opencli antigravity extract-code\`
43
+ Extract any multi-line code blocks from the current conversation view. Ideal for automated script extraction (e.g. \`opencli antigravity extract-code > script.sh\`).
44
+
45
+ ### \`opencli antigravity model <name>\`
46
+ Quickly target and switch the active LLM engine. Example: \`opencli antigravity model claude\` or \`opencli antigravity model gemini\`.
47
+
48
+ ### \`opencli antigravity watch\`
49
+ 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 @@
1
+ # Antigravity CLI Adapter (探针插件)
2
+
3
+ 🔥 **opencli 支持 CLI 化所有 electron 应用!最强大更新来袭!** 🔥
4
+
5
+ CLI all electron!现在支持把所有 electron 应用 CLI 化,从而组合出各种神奇的能力。
6
+ 如果你在使用诸如 Antigravity Ultra 等工具时觉得不够灵活或难以扩展,现在通过 OpenCLI 把他 CLI 化,轻松打破界限。
7
+
8
+ 现在,**AI 可以自己控制自己**!结合 cc/openclaw 就可以远程控制任何 electron 应用!无限玩法!!
9
+
10
+ 通过 Chrome DevTools Protocol (CDP),将你本地运行的 Antigravity 桌面客户端转变为一个完全可编程的 AI 节点。这让你可以在命令行终端中直接操控它的 UI 界面,实现真正的“零 API 限制”本地自动化大模型工作流调度。
11
+
12
+ ## 开发准备
13
+
14
+ 首先,**请在终端启动 Antigravity 桌面版**,并附加上允许远程调试(CDP)的内核启动参数:
15
+
16
+ \`\`\`bash
17
+ # 在后台启动并驻留
18
+ /Applications/Antigravity.app/Contents/MacOS/Electron \\
19
+ --remote-debugging-port=9224 \\
20
+ --remote-allow-origins="*"
21
+ \`\`\`
22
+
23
+ *(注意:如果你打包的应用重命名过主构建,可能需要把 `Electron` 换成实际的可执行文件名,如 `Antigravity`)*
24
+
25
+ 接下来,在你想执行 CLI 命令的另一个新终端板块里,声明要连入的本地调试端口环境变量:
26
+
27
+ \`\`\`bash
28
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
29
+ \`\`\`
30
+
31
+ ## 全部指令一览
32
+
33
+ ### \`opencli antigravity status\`
34
+ 快速检查当前探针与内核 CDP 的连接状态。会返回底层的当前 URL 和网页 Title。
35
+
36
+ ### \`opencli antigravity send <message>\`
37
+ 给 Agent 发送消息。它会自动定位到底部的 Lexical 输入框,安全地注入你的指定文本然后模拟回车发送。
38
+
39
+ ### \`opencli antigravity read\`
40
+ 全量抓取当前的对话面板,将所有历史聊天记录作为一整块纯文本取回。
41
+
42
+ ### \`opencli antigravity new\`
43
+ 模拟点击侧边栏顶部的“开启新对话”按钮,瞬间清空并重置 Agent 的上下文状态。
44
+
45
+ ### \`opencli antigravity extract-code\`
46
+ 从当前的 Agent 聊天记录中单独提取所有的多行代码块。非常适合自动化脚手架开发(例如直接重定向输出写入本地文件:\`opencli antigravity extract-code > script.sh\`)。
47
+
48
+ ### \`opencli antigravity model <name>\`
49
+ 切换大模型引擎。只需传入关键词(比如:\`opencli antigravity model claude\` 或 \`model gemini\`),它会自动帮你点开模型选择菜单并模拟点击。
50
+
51
+ ### \`opencli antigravity watch\`
52
+ 开启一个长连接流式监听。通过持续轮询 DOM 的变化量,它能像流式 API 一样,在终端实时向你推送 Agent 刚刚打出的那一行最新回复,直到你按 Ctrl+C 中止。
@@ -0,0 +1,42 @@
1
+ ---
2
+ description: How to automate Antigravity using OpenCLI
3
+ ---
4
+
5
+ # Antigravity Automation Skill
6
+
7
+ 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.
8
+
9
+ ## Requirements
10
+ The target Electron application MUST be launched with the remote-debugging-port flag:
11
+ \`\`\`bash
12
+ /Applications/Antigravity.app/Contents/MacOS/Electron --remote-debugging-port=9224 --remote-allow-origins="*"
13
+ \`\`\`
14
+
15
+ The agent must configure the endpoint environment variable locally before invoking standard commands:
16
+ \`\`\`bash
17
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
18
+ \`\`\`
19
+
20
+ ## High-Level Capabilities
21
+ 1. **Send Messages (`opencli antigravity send <message>`)**: Type and send a message directly into the chat UI.
22
+ 2. **Read History (`opencli antigravity read`)**: Scrape the raw chat transcript from the main UI container.
23
+ 3. **Extract Code (`opencli antigravity extract-code`)**: Automatically isolate and extract source code text blocks from the AI's recent answers.
24
+ 4. **Switch Models (`opencli antigravity model <name>`)**: Instantly toggle the active LLM (e.g., \`gemini\`, \`claude\`).
25
+ 5. **Clear Context (`opencli antigravity new`)**: Start a fresh conversation.
26
+
27
+ ## Examples for Automated Workflows
28
+
29
+ ### Generating and Saving Code
30
+ \`\`\`bash
31
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
32
+ opencli antigravity send "Write a python script to fetch HN top stories"
33
+ # wait ~10-15 seconds for output to render
34
+ opencli antigravity extract-code > hn_fetcher.py
35
+ \`\`\`
36
+
37
+ ### Reading Real-time Logs
38
+ Agents can run long-running streaming watch instances:
39
+ \`\`\`bash
40
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
41
+ opencli antigravity watch
42
+ \`\`\`
@@ -0,0 +1,30 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import * as fs from 'node:fs';
3
+
4
+ export const dumpCommand = cli({
5
+ site: 'antigravity',
6
+ name: 'dump',
7
+ description: 'Dump the DOM to help AI understand the UI',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['htmlFile', 'snapFile'],
13
+ func: async (page) => {
14
+ // Extract HTML
15
+ const html = await page.evaluate('document.body.innerHTML');
16
+ fs.writeFileSync('/tmp/antigravity-dom.html', html);
17
+
18
+ // Extract Snapshot
19
+ let snapFile = '';
20
+ try {
21
+ const snap = await page.snapshot({ raw: true });
22
+ snapFile = '/tmp/antigravity-snapshot.json';
23
+ fs.writeFileSync(snapFile, JSON.stringify(snap, null, 2));
24
+ } catch (e) {
25
+ snapFile = 'Failed';
26
+ }
27
+
28
+ return [{ htmlFile: '/tmp/antigravity-dom.html', snapFile }];
29
+ },
30
+ });
@@ -0,0 +1,34 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ export const extractCodeCommand = cli({
4
+ site: 'antigravity',
5
+ name: 'extract-code',
6
+ description: 'Extract multi-line code blocks from the current Antigravity conversation',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ args: [],
11
+ columns: ['code'],
12
+ func: async (page) => {
13
+ const blocks = await page.evaluate(`
14
+ async () => {
15
+ // Find standard pre/code blocks
16
+ let elements = Array.from(document.querySelectorAll('pre code'));
17
+
18
+ // Fallback to Monaco editor content inside the UI
19
+ if (elements.length === 0) {
20
+ elements = Array.from(document.querySelectorAll('.monaco-editor'));
21
+ }
22
+
23
+ // Generic fallback to any code tag that spans multiple lines
24
+ if (elements.length === 0) {
25
+ elements = Array.from(document.querySelectorAll('code')).filter(c => c.innerText.includes('\\n'));
26
+ }
27
+
28
+ return elements.map(el => el.innerText).filter(text => text.trim().length > 0);
29
+ }
30
+ `);
31
+
32
+ return blocks.map((code: string) => ({ code }));
33
+ },
34
+ });
@@ -0,0 +1,47 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ export const modelCommand = cli({
4
+ site: 'antigravity',
5
+ name: 'model',
6
+ description: 'Switch the active LLM model in Antigravity',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ args: [
11
+ { name: 'name', help: 'Target model name (e.g. claude, gemini, o1)', required: true, positional: true }
12
+ ],
13
+ columns: ['status'],
14
+ func: async (page, kwargs) => {
15
+ const targetName = kwargs.name.toLowerCase();
16
+
17
+ await page.evaluate(`
18
+ async () => {
19
+ const targetModelName = ${JSON.stringify(targetName)};
20
+
21
+ // 1. Locate the model selector dropdown trigger
22
+ const trigger = document.querySelector('div[aria-haspopup="dialog"] > div[tabindex="0"]');
23
+ if (!trigger) throw new Error('Could not find the model selector trigger in the UI');
24
+ trigger.click();
25
+
26
+ // 2. Wait a brief moment for React to mount the Portal/Dialog
27
+ await new Promise(r => setTimeout(r, 200));
28
+
29
+ // 3. Find the option spanning target text
30
+ const spans = Array.from(document.querySelectorAll('[role="dialog"] span'));
31
+ const target = spans.find(s => s.innerText.toLowerCase().includes(targetModelName));
32
+ if (!target) {
33
+ // If not found, click the trigger again to close it safely
34
+ trigger.click();
35
+ throw new Error('Model matching "' + targetModelName + '" was not found in the dropdown list.');
36
+ }
37
+
38
+ // 4. Click the closest parent that handles the row action
39
+ const optionNode = target.closest('.cursor-pointer') || target;
40
+ optionNode.click();
41
+ }
42
+ `);
43
+
44
+ await page.wait(0.5);
45
+ return [{ status: `Model switched to: ${kwargs.name}` }];
46
+ },
47
+ });
@@ -0,0 +1,28 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ export const newCommand = cli({
4
+ site: 'antigravity',
5
+ name: 'new',
6
+ description: 'Start a new conversation / clear context in Antigravity',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ args: [],
11
+ columns: ['status'],
12
+ func: async (page) => {
13
+ await page.evaluate(`
14
+ async () => {
15
+ const btn = document.querySelector('[data-tooltip-id="new-conversation-tooltip"]');
16
+ if (!btn) throw new Error('Could not find New Conversation button');
17
+
18
+ // In case it's disabled, we must check, but we'll try to click it anyway
19
+ btn.click();
20
+ }
21
+ `);
22
+
23
+ // Give it a moment to reset the UI
24
+ await page.wait(0.5);
25
+
26
+ return [{ status: 'Successfully started a new conversation' }];
27
+ },
28
+ });
@@ -0,0 +1,36 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ export const readCommand = cli({
4
+ site: 'antigravity',
5
+ name: 'read',
6
+ description: 'Read the latest chat messages from Antigravity AI',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ args: [
11
+ { name: 'last', help: 'Number of recent messages to read (not fully implemented due to generic structure, currently returns full history text or latest chunk)' }
12
+ ],
13
+ columns: ['role', 'content'],
14
+ func: async (page, kwargs) => {
15
+ // We execute a script inside Antigravity's Chromium environment to extract the text
16
+ // of the entire conversation pane.
17
+ const rawText = await page.evaluate(`
18
+ async () => {
19
+ const container = document.getElementById('conversation');
20
+ if (!container) throw new Error('Could not find conversation container');
21
+
22
+ // Extract the full visible text of the conversation
23
+ // In Electron/Chromium, innerText preserves basic visual line breaks nicely
24
+ return container.innerText;
25
+ }
26
+ `);
27
+
28
+ // We can do simple heuristic parsing based on typical visual markers if needed.
29
+ // For now, we return the entire text blob, or just the last 2000 characters if it's too long.
30
+ const cleanText = String(rawText).trim();
31
+ return [{
32
+ role: 'history',
33
+ content: cleanText
34
+ }];
35
+ },
36
+ });
@@ -0,0 +1,40 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ export const sendCommand = cli({
4
+ site: 'antigravity',
5
+ name: 'send',
6
+ description: 'Send a message to Antigravity AI via the internal Lexical editor',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ args: [
11
+ { name: 'message', help: 'The message text to send', required: true, positional: true }
12
+ ],
13
+ columns: ['status', 'message'],
14
+ func: async (page, kwargs) => {
15
+ const text = kwargs.message;
16
+
17
+ // We use evaluate to focus and insert text because Lexical editors maintain
18
+ // absolute control over their DOM and don't respond to raw node.textContent.
19
+ // document.execCommand simulates a native paste/typing action perfectly.
20
+ await page.evaluate(`
21
+ async () => {
22
+ const container = document.getElementById('antigravity.agentSidePanelInputBox');
23
+ if (!container) throw new Error('Could not find antigravity.agentSidePanelInputBox');
24
+ const editor = container.querySelector('[data-lexical-editor="true"]');
25
+ if (!editor) throw new Error('Could not find Antigravity input box');
26
+
27
+ editor.focus();
28
+ document.execCommand('insertText', false, ${JSON.stringify(text)});
29
+ }
30
+ `);
31
+
32
+ // Wait for the React/Lexical state to flush the new input
33
+ await page.wait(0.5);
34
+
35
+ // Press Enter to submit the message
36
+ await page.pressKey('Enter');
37
+
38
+ return [{ status: 'Sent successfully', message: text }];
39
+ },
40
+ });
@@ -0,0 +1,19 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ export const statusCommand = cli({
4
+ site: 'antigravity',
5
+ name: 'status',
6
+ description: 'Check Antigravity CDP connection and get current page state',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ args: [],
11
+ columns: ['status', 'url', 'title'],
12
+ func: async (page) => {
13
+ return {
14
+ status: 'Connected',
15
+ url: await page.evaluate('window.location.href'),
16
+ title: await page.evaluate('document.title'),
17
+ };
18
+ },
19
+ });