@jackwener/opencli 0.9.0 → 0.9.2
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.
- package/README.md +6 -2
- package/README.zh-CN.md +8 -2
- package/SKILL.md +11 -2
- package/dist/cli-manifest.json +98 -0
- package/dist/clis/codex/dump.d.ts +1 -0
- package/dist/clis/codex/dump.js +25 -0
- package/dist/clis/codex/extract-diff.d.ts +1 -0
- package/dist/clis/codex/extract-diff.js +44 -0
- package/dist/clis/codex/new.d.ts +1 -0
- package/dist/clis/codex/new.js +25 -0
- package/dist/clis/codex/read.d.ts +1 -0
- package/dist/clis/codex/read.js +36 -0
- package/dist/clis/codex/send.d.ts +1 -0
- package/dist/clis/codex/send.js +44 -0
- package/dist/clis/codex/status.d.ts +1 -0
- package/dist/clis/codex/status.js +21 -0
- package/package.json +1 -1
- package/src/clis/codex/README.md +33 -0
- package/src/clis/codex/README.zh-CN.md +33 -0
- package/src/clis/codex/dump.ts +28 -0
- package/src/clis/codex/extract-diff.ts +47 -0
- package/src/clis/codex/new.ts +29 -0
- package/src/clis/codex/read.ts +38 -0
- package/src/clis/codex/send.ts +48 -0
- package/src/clis/codex/status.ts +23 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenCLI
|
|
2
2
|
|
|
3
|
-
> **Make any website your CLI.**
|
|
3
|
+
> **Make any website or Electron App your CLI.**
|
|
4
4
|
> Zero risk · Reuse Chrome login · AI-powered discovery · 80+ commands · 19 sites
|
|
5
5
|
|
|
6
6
|
[中文文档](./README.zh-CN.md)
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
|
|
12
|
-
A CLI tool that turns **any website** into a command-line interface — Bilibili, Zhihu, 小红书, Twitter/X, Reddit, YouTube, and [many more](#built-in-commands) — powered by browser session reuse and AI-native discovery.
|
|
12
|
+
A CLI tool that turns **any website** or **Electron app** into a command-line interface — Bilibili, Zhihu, 小红书, Twitter/X, Reddit, YouTube, Antigravity, and [many more](#built-in-commands) — powered by browser session reuse and AI-native discovery.
|
|
13
|
+
|
|
14
|
+
🔥 **CLI All Electron Apps! The Most Powerful Update Has Arrived!** 🔥
|
|
15
|
+
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!
|
|
13
16
|
|
|
14
17
|
---
|
|
15
18
|
|
|
@@ -31,6 +34,7 @@ A CLI tool that turns **any website** into a command-line interface — Bilibili
|
|
|
31
34
|
|
|
32
35
|
## Highlights
|
|
33
36
|
|
|
37
|
+
- **CLI All Electron** — CLI-ify apps like Antigravity Ultra! Now AI can control itself natively using cc/openclaw!
|
|
34
38
|
- **Account-safe** — Reuses Chrome's logged-in state; your credentials never leave the browser.
|
|
35
39
|
- **AI Agent ready** — `explore` discovers APIs, `synthesize` generates adapters, `cascade` finds auth strategies.
|
|
36
40
|
- **Self-healing setup** — `opencli setup` auto-discovers tokens; `opencli doctor` diagnoses config across 10+ tools; `--fix` repairs them all.
|
package/README.zh-CN.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenCLI
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> **把任何网站或 Electron 应用变成你的命令行工具。**
|
|
4
4
|
> 零风控 · 复用 Chrome 登录 · AI 自动发现接口 · 80+ 命令 · 19 站点
|
|
5
5
|
|
|
6
6
|
[English](./README.md)
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
|
|
12
|
-
OpenCLI
|
|
12
|
+
OpenCLI 将任何网站或 Electron 应用(如 Antigravity)变成命令行工具 — B站、知乎、小红书、Twitter/X、Reddit、YouTube 等 [19 个站点](#内置命令) — 复用浏览器登录态,AI 驱动探索。
|
|
13
|
+
|
|
14
|
+
🔥 **opencli 支持 CLI 化所有 electron 应用!最强大更新来袭!** 🔥
|
|
15
|
+
CLI all electron!现在支持把所有 electron 应用 CLI 化,从而组合出各种神奇的能力。
|
|
16
|
+
如果你在使用诸如 Antigravity Ultra 等工具时觉得不够灵活或难以扩展,现在通过 OpenCLI 把他 CLI 化,轻松打破界限。
|
|
17
|
+
现在,**AI 可以自己控制自己**!结合 cc/openclaw 就可以远程控制任何 electron 应用!无限玩法!!
|
|
13
18
|
|
|
14
19
|
---
|
|
15
20
|
|
|
@@ -30,6 +35,7 @@ OpenCLI 将任何网站变成命令行工具 — B站、知乎、小红书、Twi
|
|
|
30
35
|
|
|
31
36
|
## 亮点
|
|
32
37
|
|
|
38
|
+
- **CLI All Electron** — 支持把所有 electron 应用(如 Antigravity Ultra)CLI 化,让 AI 控制自己!
|
|
33
39
|
- **多站点覆盖** — B站、知乎、小红书、Twitter、Reddit 等 19 个站点,80+ 命令
|
|
34
40
|
- **零风控** — 复用 Chrome 登录态,无需存储任何凭证
|
|
35
41
|
- **自修复配置** — `opencli setup` 自动发现 Token;`opencli doctor` 诊断 10+ 工具配置;`--fix` 一键修复
|
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: opencli
|
|
3
|
-
description: "OpenCLI — Make any website your CLI. Zero risk, AI-powered, reuse Chrome login. 80+ commands across 19 sites."
|
|
3
|
+
description: "OpenCLI — Make any website or Electron App your CLI. Zero risk, AI-powered, reuse Chrome login. 80+ commands across 19 sites."
|
|
4
4
|
version: 0.7.3
|
|
5
5
|
author: jackwener
|
|
6
6
|
tags: [cli, browser, web, mcp, playwright, bilibili, zhihu, twitter, github, v2ex, hackernews, reddit, xiaohongshu, xueqiu, youtube, boss, coupang, AI, agent]
|
|
@@ -8,7 +8,7 @@ tags: [cli, browser, web, mcp, playwright, bilibili, zhihu, twitter, github, v2e
|
|
|
8
8
|
|
|
9
9
|
# OpenCLI
|
|
10
10
|
|
|
11
|
-
> Make any website your CLI. Reuse Chrome login, zero risk, AI-powered discovery.
|
|
11
|
+
> Make any website or Electron App your CLI. Reuse Chrome login, zero risk, AI-powered discovery.
|
|
12
12
|
|
|
13
13
|
> [!CAUTION]
|
|
14
14
|
> **AI Agent 必读:创建或修改任何适配器之前,你必须先阅读 [CLI-EXPLORER.md](./CLI-EXPLORER.md)!**
|
|
@@ -151,6 +151,15 @@ opencli smzdm search --keyword "耳机" # 搜索好价
|
|
|
151
151
|
|
|
152
152
|
# 携程 (browser)
|
|
153
153
|
opencli ctrip search --query "三亚" # 搜索目的地
|
|
154
|
+
|
|
155
|
+
# Antigravity (Electron/CDP)
|
|
156
|
+
opencli antigravity status # 检查 CDP 连接
|
|
157
|
+
opencli antigravity send "hello" # 发送文本到当前 agent 聊天框
|
|
158
|
+
opencli antigravity read # 读取整个聊天记录面板
|
|
159
|
+
opencli antigravity new # 清空聊天、开启新对话
|
|
160
|
+
opencli antigravity extract-code # 自动抽取 AI 回复中的代码块
|
|
161
|
+
opencli antigravity model claude # 切换底层模型
|
|
162
|
+
opencli antigravity watch # 流式监听增量消息
|
|
154
163
|
```
|
|
155
164
|
|
|
156
165
|
### Management Commands
|
package/dist/cli-manifest.json
CHANGED
|
@@ -810,6 +810,104 @@
|
|
|
810
810
|
"url"
|
|
811
811
|
]
|
|
812
812
|
},
|
|
813
|
+
{
|
|
814
|
+
"site": "codex",
|
|
815
|
+
"name": "dump",
|
|
816
|
+
"description": "Dump the DOM and Accessibility tree of Codex for reverse-engineering",
|
|
817
|
+
"strategy": "ui",
|
|
818
|
+
"browser": true,
|
|
819
|
+
"args": [],
|
|
820
|
+
"type": "ts",
|
|
821
|
+
"modulePath": "codex/dump.js",
|
|
822
|
+
"domain": "localhost",
|
|
823
|
+
"columns": [
|
|
824
|
+
"action",
|
|
825
|
+
"files"
|
|
826
|
+
]
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
"site": "codex",
|
|
830
|
+
"name": "extract-diff",
|
|
831
|
+
"description": "Extract visual code review diff patches from Codex",
|
|
832
|
+
"strategy": "ui",
|
|
833
|
+
"browser": true,
|
|
834
|
+
"args": [],
|
|
835
|
+
"type": "ts",
|
|
836
|
+
"modulePath": "codex/extract-diff.js",
|
|
837
|
+
"domain": "localhost",
|
|
838
|
+
"columns": [
|
|
839
|
+
"File",
|
|
840
|
+
"Diff"
|
|
841
|
+
]
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
"site": "codex",
|
|
845
|
+
"name": "new",
|
|
846
|
+
"description": "Start a new Codex conversation thread / isolated workspace",
|
|
847
|
+
"strategy": "ui",
|
|
848
|
+
"browser": true,
|
|
849
|
+
"args": [],
|
|
850
|
+
"type": "ts",
|
|
851
|
+
"modulePath": "codex/new.js",
|
|
852
|
+
"domain": "localhost",
|
|
853
|
+
"columns": [
|
|
854
|
+
"Status",
|
|
855
|
+
"Action"
|
|
856
|
+
]
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
"site": "codex",
|
|
860
|
+
"name": "read",
|
|
861
|
+
"description": "Read the contents of the current Codex conversation thread",
|
|
862
|
+
"strategy": "ui",
|
|
863
|
+
"browser": true,
|
|
864
|
+
"args": [],
|
|
865
|
+
"type": "ts",
|
|
866
|
+
"modulePath": "codex/read.js",
|
|
867
|
+
"domain": "localhost",
|
|
868
|
+
"columns": [
|
|
869
|
+
"Thread_Content"
|
|
870
|
+
]
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
"site": "codex",
|
|
874
|
+
"name": "send",
|
|
875
|
+
"description": "Send text/commands to the Codex AI composer",
|
|
876
|
+
"strategy": "ui",
|
|
877
|
+
"browser": true,
|
|
878
|
+
"args": [
|
|
879
|
+
{
|
|
880
|
+
"name": "text",
|
|
881
|
+
"type": "str",
|
|
882
|
+
"required": true,
|
|
883
|
+
"positional": true,
|
|
884
|
+
"help": "Text, command (e.g. /review), or skill (e.g. $imagegen)"
|
|
885
|
+
}
|
|
886
|
+
],
|
|
887
|
+
"type": "ts",
|
|
888
|
+
"modulePath": "codex/send.js",
|
|
889
|
+
"domain": "localhost",
|
|
890
|
+
"columns": [
|
|
891
|
+
"Status",
|
|
892
|
+
"InjectedText"
|
|
893
|
+
]
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
"site": "codex",
|
|
897
|
+
"name": "status",
|
|
898
|
+
"description": "Check active CDP connection to OpenAI Codex App",
|
|
899
|
+
"strategy": "ui",
|
|
900
|
+
"browser": true,
|
|
901
|
+
"args": [],
|
|
902
|
+
"type": "ts",
|
|
903
|
+
"modulePath": "codex/status.js",
|
|
904
|
+
"domain": "localhost",
|
|
905
|
+
"columns": [
|
|
906
|
+
"Status",
|
|
907
|
+
"Url",
|
|
908
|
+
"Title"
|
|
909
|
+
]
|
|
910
|
+
},
|
|
813
911
|
{
|
|
814
912
|
"site": "coupang",
|
|
815
913
|
"name": "add-to-cart",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const dumpCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
export const dumpCommand = cli({
|
|
4
|
+
site: 'codex',
|
|
5
|
+
name: 'dump',
|
|
6
|
+
description: 'Dump the DOM and Accessibility tree of Codex for reverse-engineering',
|
|
7
|
+
domain: 'localhost',
|
|
8
|
+
strategy: Strategy.UI,
|
|
9
|
+
browser: true,
|
|
10
|
+
columns: ['action', 'files'],
|
|
11
|
+
func: async (page) => {
|
|
12
|
+
// Extract full HTML
|
|
13
|
+
const dom = await page.evaluate('document.body.innerHTML');
|
|
14
|
+
fs.writeFileSync('/tmp/codex-dom.html', dom);
|
|
15
|
+
// Get accessibility snapshot
|
|
16
|
+
const snap = await page.snapshot({ interactive: false });
|
|
17
|
+
fs.writeFileSync('/tmp/codex-snapshot.json', JSON.stringify(snap, null, 2));
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
action: 'Dom extraction finished',
|
|
21
|
+
files: '/tmp/codex-dom.html, /tmp/codex-snapshot.json',
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const extractDiffCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
export const extractDiffCommand = cli({
|
|
3
|
+
site: 'codex',
|
|
4
|
+
name: 'extract-diff',
|
|
5
|
+
description: 'Extract visual code review diff patches from Codex',
|
|
6
|
+
domain: 'localhost',
|
|
7
|
+
strategy: Strategy.UI,
|
|
8
|
+
browser: true,
|
|
9
|
+
columns: ['File', 'Diff'],
|
|
10
|
+
func: async (page) => {
|
|
11
|
+
const diffs = await page.evaluate(`
|
|
12
|
+
(function() {
|
|
13
|
+
const results = [];
|
|
14
|
+
// Assuming diffs are rendered with standard diff classes or monaco difference editors
|
|
15
|
+
const diffBlocks = document.querySelectorAll('.diff-editor, .monaco-diff-editor, [data-testid="diff-view"]');
|
|
16
|
+
|
|
17
|
+
diffBlocks.forEach((block, index) => {
|
|
18
|
+
// Very roughly scrape text representing additions/deletions mapped from the inner wrapper
|
|
19
|
+
results.push({
|
|
20
|
+
File: block.getAttribute('data-filename') || \`DiffBlock_\${index+1}\`,
|
|
21
|
+
Diff: block.innerText || block.textContent
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// If no structured diffs found, try to find any code blocks labeled as patches
|
|
26
|
+
if (results.length === 0) {
|
|
27
|
+
const codeBlocks = document.querySelectorAll('pre code.language-diff, pre code.language-patch');
|
|
28
|
+
codeBlocks.forEach((code, index) => {
|
|
29
|
+
results.push({
|
|
30
|
+
File: \`Patch_\${index+1}\`,
|
|
31
|
+
Diff: code.innerText || code.textContent
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return results;
|
|
37
|
+
})()
|
|
38
|
+
`);
|
|
39
|
+
if (diffs.length === 0) {
|
|
40
|
+
return [{ File: 'No diffs found', Diff: 'Try running opencli codex send "/review" first' }];
|
|
41
|
+
}
|
|
42
|
+
return diffs;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const newCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
export const newCommand = cli({
|
|
3
|
+
site: 'codex',
|
|
4
|
+
name: 'new',
|
|
5
|
+
description: 'Start a new Codex conversation thread / isolated workspace',
|
|
6
|
+
domain: 'localhost',
|
|
7
|
+
strategy: Strategy.UI,
|
|
8
|
+
browser: true,
|
|
9
|
+
columns: ['Status', 'Action'],
|
|
10
|
+
func: async (page) => {
|
|
11
|
+
// According to research, Cmd+N / Ctrl+N spins up a new thread
|
|
12
|
+
const isMac = process.platform === 'darwin';
|
|
13
|
+
const newThreadKey = isMac ? 'Meta+N' : 'Control+N';
|
|
14
|
+
// Simulate keyboard shortcut
|
|
15
|
+
await page.pressKey(newThreadKey);
|
|
16
|
+
// Wait a brief moment for UI animation
|
|
17
|
+
await page.wait(1);
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
Status: 'Success',
|
|
21
|
+
Action: `Pressed ${newThreadKey} to trigger New Thread`,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const readCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
export const readCommand = cli({
|
|
3
|
+
site: 'codex',
|
|
4
|
+
name: 'read',
|
|
5
|
+
description: 'Read the contents of the current Codex conversation thread',
|
|
6
|
+
domain: 'localhost',
|
|
7
|
+
strategy: Strategy.UI,
|
|
8
|
+
browser: true,
|
|
9
|
+
columns: ['Thread_Content'],
|
|
10
|
+
func: async (page) => {
|
|
11
|
+
const historyText = await page.evaluate(`
|
|
12
|
+
(function() {
|
|
13
|
+
// Precise Codex selector for chat messages
|
|
14
|
+
const turns = Array.from(document.querySelectorAll('[data-content-search-turn-key]'));
|
|
15
|
+
if (turns.length > 0) {
|
|
16
|
+
return turns.map(t => t.innerText || t.textContent).join('\\n\\n---\\n\\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Fallback robust scraping heuristic for chat history panes
|
|
20
|
+
const threadContainer = document.querySelector('[role="log"], [data-testid="conversation"], .thread-container, .messages-list, main');
|
|
21
|
+
|
|
22
|
+
if (threadContainer) {
|
|
23
|
+
return threadContainer.innerText || threadContainer.textContent;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// If specific containers fail, just dump the whole body's readable text minus the navigation
|
|
27
|
+
return document.body.innerText;
|
|
28
|
+
})()
|
|
29
|
+
`);
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
Thread_Content: historyText,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const sendCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
export const sendCommand = cli({
|
|
3
|
+
site: 'codex',
|
|
4
|
+
name: 'send',
|
|
5
|
+
description: 'Send text/commands to the Codex AI composer',
|
|
6
|
+
domain: 'localhost',
|
|
7
|
+
strategy: Strategy.UI,
|
|
8
|
+
browser: true,
|
|
9
|
+
args: [{ name: 'text', required: true, positional: true, help: 'Text, command (e.g. /review), or skill (e.g. $imagegen)' }],
|
|
10
|
+
columns: ['Status', 'InjectedText'],
|
|
11
|
+
func: async (page, kwargs) => {
|
|
12
|
+
const textToInsert = kwargs.text;
|
|
13
|
+
// We use evaluate to inject text bypassing complex nested shadow roots or contenteditables
|
|
14
|
+
await page.evaluate(`
|
|
15
|
+
(function(text) {
|
|
16
|
+
// Attempt 1: Look for standard textarea/composer input
|
|
17
|
+
let composer = document.querySelector('textarea, [contenteditable="true"]');
|
|
18
|
+
|
|
19
|
+
// Basic heuristic: prioritize elements that are deeply nested, visible, and have 'composer' or 'input' classes
|
|
20
|
+
const editables = Array.from(document.querySelectorAll('[contenteditable="true"]'));
|
|
21
|
+
if (editables.length > 0) {
|
|
22
|
+
composer = editables[editables.length - 1]; // Often the active input is appended near the end
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!composer) {
|
|
26
|
+
throw new Error('Could not find Composer input element in Codex UI');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
composer.focus();
|
|
30
|
+
|
|
31
|
+
// This handles Lexical/ProseMirror/Monaco rich-text editors effectively by mimicking human paste/type deeply.
|
|
32
|
+
document.execCommand('insertText', false, text);
|
|
33
|
+
})(${JSON.stringify(textToInsert)})
|
|
34
|
+
`);
|
|
35
|
+
// Simulate Enter key to submit
|
|
36
|
+
await page.pressKey('Enter');
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
Status: 'Success',
|
|
40
|
+
InjectedText: textToInsert,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const statusCommand: import("../../registry.js").CliCommand;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
export const statusCommand = cli({
|
|
3
|
+
site: 'codex',
|
|
4
|
+
name: 'status',
|
|
5
|
+
description: 'Check active CDP connection to OpenAI Codex App',
|
|
6
|
+
domain: 'localhost',
|
|
7
|
+
strategy: Strategy.UI, // Interactive UI manipulation
|
|
8
|
+
browser: true,
|
|
9
|
+
columns: ['Status', 'Url', 'Title'],
|
|
10
|
+
func: async (page) => {
|
|
11
|
+
const url = await page.evaluate('window.location.href');
|
|
12
|
+
const title = await page.evaluate('document.title');
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
Status: 'Connected',
|
|
16
|
+
Url: url,
|
|
17
|
+
Title: title,
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
},
|
|
21
|
+
});
|
package/package.json
CHANGED
|
@@ -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,38 @@
|
|
|
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
|
+
// Precise Codex selector for chat messages
|
|
15
|
+
const turns = Array.from(document.querySelectorAll('[data-content-search-turn-key]'));
|
|
16
|
+
if (turns.length > 0) {
|
|
17
|
+
return turns.map(t => t.innerText || t.textContent).join('\\n\\n---\\n\\n');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Fallback robust scraping heuristic for chat history panes
|
|
21
|
+
const threadContainer = document.querySelector('[role="log"], [data-testid="conversation"], .thread-container, .messages-list, main');
|
|
22
|
+
|
|
23
|
+
if (threadContainer) {
|
|
24
|
+
return threadContainer.innerText || threadContainer.textContent;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// If specific containers fail, just dump the whole body's readable text minus the navigation
|
|
28
|
+
return document.body.innerText;
|
|
29
|
+
})()
|
|
30
|
+
`);
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
Thread_Content: historyText,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -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, positional: 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
|
+
});
|