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