feat(codex): implement generic CDP adapters for OpenAI Codex desktop app

This commit is contained in:
jackwener committed 2026-03-18 17:06:47 +08:00
1 parent abac070ce4
commit 3cc2cb5504
8 files changed
+274

No files matched your search

+33
View File
@@ -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.
+33
View File
@@ -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 修改块,直接输出结构化数据!
+28
View File
@@ -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',
},
];
},
});
+47
View File
@@ -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;
},
});
+29
View File
@@ -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`,
},
];
},
});
+33
View File
@@ -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,
},
];
},
});
+48
View File
@@ -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,
},
];
},
});
+23
View File
@@ -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,
},
];
},
});