@jackwener/opencli 0.8.0 → 0.9.1

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 (68) hide show
  1. package/README.md +7 -2
  2. package/README.zh-CN.md +9 -2
  3. package/SKILL.md +11 -2
  4. package/dist/cli-manifest.json +343 -0
  5. package/dist/clis/antigravity/dump.d.ts +1 -0
  6. package/dist/clis/antigravity/dump.js +28 -0
  7. package/dist/clis/antigravity/extract-code.d.ts +1 -0
  8. package/dist/clis/antigravity/extract-code.js +32 -0
  9. package/dist/clis/antigravity/model.d.ts +1 -0
  10. package/dist/clis/antigravity/model.js +44 -0
  11. package/dist/clis/antigravity/new.d.ts +1 -0
  12. package/dist/clis/antigravity/new.js +25 -0
  13. package/dist/clis/antigravity/read.d.ts +1 -0
  14. package/dist/clis/antigravity/read.js +34 -0
  15. package/dist/clis/antigravity/send.d.ts +1 -0
  16. package/dist/clis/antigravity/send.js +35 -0
  17. package/dist/clis/antigravity/status.d.ts +1 -0
  18. package/dist/clis/antigravity/status.js +18 -0
  19. package/dist/clis/antigravity/watch.d.ts +1 -0
  20. package/dist/clis/antigravity/watch.js +41 -0
  21. package/dist/clis/codex/dump.d.ts +1 -0
  22. package/dist/clis/codex/dump.js +25 -0
  23. package/dist/clis/codex/extract-diff.d.ts +1 -0
  24. package/dist/clis/codex/extract-diff.js +44 -0
  25. package/dist/clis/codex/new.d.ts +1 -0
  26. package/dist/clis/codex/new.js +25 -0
  27. package/dist/clis/codex/read.d.ts +1 -0
  28. package/dist/clis/codex/read.js +31 -0
  29. package/dist/clis/codex/send.d.ts +1 -0
  30. package/dist/clis/codex/send.js +44 -0
  31. package/dist/clis/codex/status.d.ts +1 -0
  32. package/dist/clis/codex/status.js +21 -0
  33. package/dist/clis/xiaoyuzhou/episode.d.ts +1 -0
  34. package/dist/clis/xiaoyuzhou/episode.js +28 -0
  35. package/dist/clis/xiaoyuzhou/podcast-episodes.d.ts +1 -0
  36. package/dist/clis/xiaoyuzhou/podcast-episodes.js +36 -0
  37. package/dist/clis/xiaoyuzhou/podcast.d.ts +1 -0
  38. package/dist/clis/xiaoyuzhou/podcast.js +27 -0
  39. package/dist/clis/xiaoyuzhou/utils.d.ts +16 -0
  40. package/dist/clis/xiaoyuzhou/utils.js +55 -0
  41. package/dist/clis/xiaoyuzhou/utils.test.d.ts +1 -0
  42. package/dist/clis/xiaoyuzhou/utils.test.js +99 -0
  43. package/package.json +1 -1
  44. package/src/clis/antigravity/README.md +49 -0
  45. package/src/clis/antigravity/README.zh-CN.md +52 -0
  46. package/src/clis/antigravity/SKILL.md +42 -0
  47. package/src/clis/antigravity/dump.ts +30 -0
  48. package/src/clis/antigravity/extract-code.ts +34 -0
  49. package/src/clis/antigravity/model.ts +47 -0
  50. package/src/clis/antigravity/new.ts +28 -0
  51. package/src/clis/antigravity/read.ts +36 -0
  52. package/src/clis/antigravity/send.ts +40 -0
  53. package/src/clis/antigravity/status.ts +19 -0
  54. package/src/clis/antigravity/watch.ts +45 -0
  55. package/src/clis/codex/README.md +33 -0
  56. package/src/clis/codex/README.zh-CN.md +33 -0
  57. package/src/clis/codex/dump.ts +28 -0
  58. package/src/clis/codex/extract-diff.ts +47 -0
  59. package/src/clis/codex/new.ts +29 -0
  60. package/src/clis/codex/read.ts +33 -0
  61. package/src/clis/codex/send.ts +48 -0
  62. package/src/clis/codex/status.ts +23 -0
  63. package/src/clis/xiaoyuzhou/episode.ts +28 -0
  64. package/src/clis/xiaoyuzhou/podcast-episodes.ts +36 -0
  65. package/src/clis/xiaoyuzhou/podcast.ts +27 -0
  66. package/src/clis/xiaoyuzhou/utils.test.ts +122 -0
  67. package/src/clis/xiaoyuzhou/utils.ts +65 -0
  68. package/tests/e2e/public-commands.test.ts +62 -0
@@ -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
+ });
@@ -0,0 +1,45 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ export const watchCommand = cli({
4
+ site: 'antigravity',
5
+ name: 'watch',
6
+ description: 'Stream new chat messages from Antigravity in real-time',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ args: [],
11
+ timeoutSeconds: 86400, // Run for up to 24 hours
12
+ columns: [], // We use direct stdout streaming
13
+ func: async (page) => {
14
+ console.log('Watching Antigravity chat... (Press Ctrl+C to stop)');
15
+
16
+ let lastLength = 0;
17
+
18
+ // Loop until process gets killed
19
+ while (true) {
20
+ const text = await page.evaluate(`
21
+ async () => {
22
+ const container = document.getElementById('conversation');
23
+ return container ? container.innerText : '';
24
+ }
25
+ `);
26
+
27
+ const currentLength = text.length;
28
+ if (currentLength > lastLength) {
29
+ // Delta mode
30
+ const newSegment = text.substring(lastLength);
31
+ if (newSegment.trim().length > 0) {
32
+ process.stdout.write(newSegment);
33
+ }
34
+ lastLength = currentLength;
35
+ } else if (currentLength < lastLength) {
36
+ // The conversation was cleared or updated significantly
37
+ lastLength = currentLength;
38
+ console.log('\\n--- Conversation Cleared/Changed ---\\n');
39
+ process.stdout.write(text);
40
+ }
41
+
42
+ await new Promise(resolve => setTimeout(resolve, 500));
43
+ }
44
+ },
45
+ });
@@ -0,0 +1,33 @@
1
+ # OpenAI Codex Adapter for OpenCLI
2
+
3
+ Control the **OpenAI Codex Desktop App** headless or headfully via Chrome DevTools Protocol (CDP).
4
+ Because Codex is built on Electron, OpenCLI can directly drive its internal UI, automate slash commands, and manipulate its AI agent threads.
5
+
6
+ ## Prerequisites
7
+
8
+ 1. You must have the official OpenAI Codex app installed.
9
+ 2. Launch it via the terminal and expose the remote debugging port:
10
+ ```bash
11
+ # macOS
12
+ /Applications/Codex.app/Contents/MacOS/Codex --remote-debugging-port=9222
13
+ ```
14
+
15
+ ## Setup
16
+
17
+ Export the CDP endpoint in your shell:
18
+ ```bash
19
+ export OPENCLI_CODEX_CDP_ENDPOINT="http://127.0.0.1:9222"
20
+ ```
21
+
22
+ ## Commands
23
+
24
+ ### Diagnostics
25
+ - `opencli codex status`: Checks connection and reads the current active window URL/title.
26
+ - `opencli codex dump`: Dumps the full UI DOM and Accessibility tree into `/tmp` (ideal for building AI automation tools on top of it).
27
+
28
+ ### Agent Manipulation
29
+ - `opencli codex new`: Simulates `Cmd+N` to start a completely fresh and isolated Git Worktree thread context.
30
+ - `opencli codex send "message"`: Robustly finds the active Thread Composer and injects your text.
31
+ - *Pro-tip*: You can trigger internal shortcuts by sending them, e.g., `opencli codex send "/review"` or `opencli codex send "$imagegen draw a cat"`.
32
+ - `opencli codex read`: Extracts the entire current thread history and AI reasoning logs into readable text.
33
+ - `opencli codex extract-diff`: Automatically scrapes any visual Patch chunks and Code Diffs the AI generated inside the review UI.
@@ -0,0 +1,33 @@
1
+ # OpenAI Codex 桌面端适配器 (OpenCLI)
2
+
3
+ 利用 CDP 协议,直接从命令行/外部脚本接管和操控 **OpenAI Codex 官方桌面版**。
4
+ 因为官方 Codex 是基于 Electron 构建的“多 Agent 协作中心”,通过本适配器,你可以让 AI 自动控制另一个 AI 完成工作,甚至自动截取代码审查的 Diff!
5
+
6
+ ## 前置环境准备
7
+
8
+ 1. 你必须下载并安装了官方原版的 OpenAI Codex 客户端。
9
+ 2. 必须通过命令行挂载 CDP 调试端口启动它:
10
+ ```bash
11
+ # macOS 启动示例
12
+ /Applications/Codex.app/Contents/MacOS/Codex --remote-debugging-port=9222
13
+ ```
14
+
15
+ ## 配置指南
16
+
17
+ 在你要运行命令的终端里导出环境变量:
18
+ ```bash
19
+ export OPENCLI_CODEX_CDP_ENDPOINT="http://127.0.0.1:9222"
20
+ ```
21
+
22
+ ## 核心指令
23
+
24
+ ### 探查与调试
25
+ - `opencli codex status`: 检查是否成功连上内部 Chromium,获取上下文 Title。
26
+ - `opencli codex dump`: 强制剥离整个 App 的内部 DOM 树和无障碍视图并保存到 `/tmp`,是编写复杂自动化 RPA 脚本的终极利刃。
27
+
28
+ ### 自动化执行
29
+ - `opencli codex new`: 模拟按下 `Cmd+N`。建立一个彻底干净、隔离了 Git Worktree 的全线并行 Thread。
30
+ - `opencli codex send "要发送的话"`: 强行跨越 Shadow Root 找到对应的富文本编辑器并注入提词。
31
+ - *高阶技巧*: 你可以直接发送内置宏!例如 `opencli codex send "/review"` 就能触发本工作流的代码审查,或者 `opencli codex send "$imagegen"` 触发技能。
32
+ - `opencli codex read`: 完整抓取并提取整个当前 Thread 里的思考过程和对话日志。
33
+ - `opencli codex extract-diff`: 专门用于拦截并提取由 AI 建议的 `+` / `-` 代码 Patch 修改块,直接输出结构化数据!
@@ -0,0 +1,28 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import * as fs from 'fs';
3
+
4
+ export const dumpCommand = cli({
5
+ site: 'codex',
6
+ name: 'dump',
7
+ description: 'Dump the DOM and Accessibility tree of Codex for reverse-engineering',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ columns: ['action', 'files'],
12
+ func: async (page) => {
13
+ // Extract full HTML
14
+ const dom = await page.evaluate('document.body.innerHTML');
15
+ fs.writeFileSync('/tmp/codex-dom.html', dom);
16
+
17
+ // Get accessibility snapshot
18
+ const snap = await page.snapshot({ interactive: false });
19
+ fs.writeFileSync('/tmp/codex-snapshot.json', JSON.stringify(snap, null, 2));
20
+
21
+ return [
22
+ {
23
+ action: 'Dom extraction finished',
24
+ files: '/tmp/codex-dom.html, /tmp/codex-snapshot.json',
25
+ },
26
+ ];
27
+ },
28
+ });
@@ -0,0 +1,47 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ export const extractDiffCommand = cli({
4
+ site: 'codex',
5
+ name: 'extract-diff',
6
+ description: 'Extract visual code review diff patches from Codex',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ columns: ['File', 'Diff'],
11
+ func: async (page) => {
12
+ const diffs = await page.evaluate(`
13
+ (function() {
14
+ const results = [];
15
+ // Assuming diffs are rendered with standard diff classes or monaco difference editors
16
+ const diffBlocks = document.querySelectorAll('.diff-editor, .monaco-diff-editor, [data-testid="diff-view"]');
17
+
18
+ diffBlocks.forEach((block, index) => {
19
+ // Very roughly scrape text representing additions/deletions mapped from the inner wrapper
20
+ results.push({
21
+ File: block.getAttribute('data-filename') || \`DiffBlock_\${index+1}\`,
22
+ Diff: block.innerText || block.textContent
23
+ });
24
+ });
25
+
26
+ // If no structured diffs found, try to find any code blocks labeled as patches
27
+ if (results.length === 0) {
28
+ const codeBlocks = document.querySelectorAll('pre code.language-diff, pre code.language-patch');
29
+ codeBlocks.forEach((code, index) => {
30
+ results.push({
31
+ File: \`Patch_\${index+1}\`,
32
+ Diff: code.innerText || code.textContent
33
+ });
34
+ });
35
+ }
36
+
37
+ return results;
38
+ })()
39
+ `);
40
+
41
+ if (diffs.length === 0) {
42
+ return [{ File: 'No diffs found', Diff: 'Try running opencli codex send "/review" first' }];
43
+ }
44
+
45
+ return diffs;
46
+ },
47
+ });
@@ -0,0 +1,29 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ export const newCommand = cli({
4
+ site: 'codex',
5
+ name: 'new',
6
+ description: 'Start a new Codex conversation thread / isolated workspace',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ columns: ['Status', 'Action'],
11
+ func: async (page) => {
12
+ // According to research, Cmd+N / Ctrl+N spins up a new thread
13
+ const isMac = process.platform === 'darwin';
14
+ const newThreadKey = isMac ? 'Meta+N' : 'Control+N';
15
+
16
+ // Simulate keyboard shortcut
17
+ await page.pressKey(newThreadKey);
18
+
19
+ // Wait a brief moment for UI animation
20
+ await page.wait(1);
21
+
22
+ return [
23
+ {
24
+ Status: 'Success',
25
+ Action: `Pressed ${newThreadKey} to trigger New Thread`,
26
+ },
27
+ ];
28
+ },
29
+ });
@@ -0,0 +1,33 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ export const readCommand = cli({
4
+ site: 'codex',
5
+ name: 'read',
6
+ description: 'Read the contents of the current Codex conversation thread',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ columns: ['Thread_Content'],
11
+ func: async (page) => {
12
+ const historyText = await page.evaluate(`
13
+ (function() {
14
+ // Fallback robust scraping heuristic for chat history panes
15
+ // We look for large scrolling areas or generic message lists
16
+ const threadContainer = document.querySelector('[role="log"], [data-testid="conversation"], main, .thread-container, .messages-list');
17
+
18
+ if (threadContainer) {
19
+ return threadContainer.innerText || threadContainer.textContent;
20
+ }
21
+
22
+ // If specific containers fail, just dump the whole body's readable text minus the navigation
23
+ return document.body.innerText;
24
+ })()
25
+ `);
26
+
27
+ return [
28
+ {
29
+ Thread_Content: historyText,
30
+ },
31
+ ];
32
+ },
33
+ });
@@ -0,0 +1,48 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ export const sendCommand = cli({
4
+ site: 'codex',
5
+ name: 'send',
6
+ description: 'Send text/commands to the Codex AI composer',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ args: [{ name: 'text', required: true, help: 'Text, command (e.g. /review), or skill (e.g. $imagegen)' }],
11
+ columns: ['Status', 'InjectedText'],
12
+ func: async (page, kwargs) => {
13
+ const textToInsert = kwargs.text as string;
14
+
15
+ // We use evaluate to inject text bypassing complex nested shadow roots or contenteditables
16
+ await page.evaluate(`
17
+ (function(text) {
18
+ // Attempt 1: Look for standard textarea/composer input
19
+ let composer = document.querySelector('textarea, [contenteditable="true"]');
20
+
21
+ // Basic heuristic: prioritize elements that are deeply nested, visible, and have 'composer' or 'input' classes
22
+ const editables = Array.from(document.querySelectorAll('[contenteditable="true"]'));
23
+ if (editables.length > 0) {
24
+ composer = editables[editables.length - 1]; // Often the active input is appended near the end
25
+ }
26
+
27
+ if (!composer) {
28
+ throw new Error('Could not find Composer input element in Codex UI');
29
+ }
30
+
31
+ composer.focus();
32
+
33
+ // This handles Lexical/ProseMirror/Monaco rich-text editors effectively by mimicking human paste/type deeply.
34
+ document.execCommand('insertText', false, text);
35
+ })(${JSON.stringify(textToInsert)})
36
+ `);
37
+
38
+ // Simulate Enter key to submit
39
+ await page.pressKey('Enter');
40
+
41
+ return [
42
+ {
43
+ Status: 'Success',
44
+ InjectedText: textToInsert,
45
+ },
46
+ ];
47
+ },
48
+ });
@@ -0,0 +1,23 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ export const statusCommand = cli({
4
+ site: 'codex',
5
+ name: 'status',
6
+ description: 'Check active CDP connection to OpenAI Codex App',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI, // Interactive UI manipulation
9
+ browser: true,
10
+ columns: ['Status', 'Url', 'Title'],
11
+ func: async (page) => {
12
+ const url = await page.evaluate('window.location.href');
13
+ const title = await page.evaluate('document.title');
14
+
15
+ return [
16
+ {
17
+ Status: 'Connected',
18
+ Url: url,
19
+ Title: title,
20
+ },
21
+ ];
22
+ },
23
+ });