@jackwener/opencli 0.9.4 → 0.9.6
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 +5 -10
- package/README.zh-CN.md +4 -3
- package/dist/cli-manifest.json +287 -4
- package/dist/clis/antigravity/model.js +2 -2
- package/dist/clis/antigravity/send.js +2 -2
- package/dist/clis/chatgpt/ask.d.ts +1 -0
- package/dist/clis/chatgpt/ask.js +68 -0
- package/dist/clis/chatgpt/new.d.ts +1 -0
- package/dist/clis/chatgpt/new.js +23 -0
- package/dist/clis/chatgpt/read.d.ts +1 -0
- package/dist/clis/chatgpt/read.js +28 -0
- package/dist/clis/chatgpt/send.d.ts +1 -0
- package/dist/clis/chatgpt/send.js +42 -0
- package/dist/clis/chatgpt/status.d.ts +1 -0
- package/dist/clis/chatgpt/status.js +21 -0
- package/dist/clis/codex/ask.d.ts +1 -0
- package/dist/clis/codex/ask.js +67 -0
- package/dist/clis/codex/export.d.ts +1 -0
- package/dist/clis/codex/export.js +37 -0
- package/dist/clis/codex/history.d.ts +1 -0
- package/dist/clis/codex/history.js +43 -0
- package/dist/clis/codex/read.js +3 -5
- package/dist/clis/codex/screenshot.d.ts +1 -0
- package/dist/clis/codex/screenshot.js +27 -0
- package/dist/clis/codex/send.js +3 -6
- package/dist/clis/codex/status.js +2 -1
- package/dist/clis/cursor/ask.d.ts +1 -0
- package/dist/clis/cursor/ask.js +69 -0
- package/dist/clis/cursor/composer.js +9 -28
- package/dist/clis/cursor/export.d.ts +1 -0
- package/dist/clis/cursor/export.js +51 -0
- package/dist/clis/cursor/history.d.ts +1 -0
- package/dist/clis/cursor/history.js +43 -0
- package/dist/clis/cursor/new.js +4 -13
- package/dist/clis/cursor/screenshot.d.ts +1 -0
- package/dist/clis/cursor/screenshot.js +31 -0
- package/package.json +1 -1
- package/src/clis/antigravity/README.md +2 -3
- package/src/clis/antigravity/README.zh-CN.md +2 -3
- package/src/clis/antigravity/SKILL.md +1 -1
- package/src/clis/antigravity/model.ts +2 -2
- package/src/clis/antigravity/send.ts +2 -2
- package/src/clis/chatgpt/README.md +44 -0
- package/src/clis/chatgpt/README.zh-CN.md +44 -0
- package/src/clis/chatgpt/ask.ts +77 -0
- package/src/clis/chatgpt/new.ts +24 -0
- package/src/clis/chatgpt/read.ts +32 -0
- package/src/clis/chatgpt/send.ts +48 -0
- package/src/clis/chatgpt/status.ts +22 -0
- package/src/clis/codex/README.md +1 -0
- package/src/clis/codex/ask.ts +77 -0
- package/src/clis/codex/export.ts +42 -0
- package/src/clis/codex/extract-diff.ts +1 -0
- package/src/clis/codex/history.ts +47 -0
- package/src/clis/codex/read.ts +5 -6
- package/src/clis/codex/screenshot.ts +33 -0
- package/src/clis/codex/send.ts +6 -7
- package/src/clis/codex/status.ts +4 -2
- package/src/clis/cursor/README.md +33 -0
- package/src/clis/cursor/README.zh-CN.md +33 -0
- package/src/clis/cursor/ask.ts +81 -0
- package/src/clis/cursor/composer.ts +9 -30
- package/src/clis/cursor/export.ts +57 -0
- package/src/clis/cursor/history.ts +47 -0
- package/src/clis/cursor/new.ts +4 -15
- package/src/clis/cursor/screenshot.ts +38 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# ChatGPT Desktop Adapter for OpenCLI
|
|
2
|
+
|
|
3
|
+
Control the **ChatGPT macOS Desktop App** directly from the terminal. OpenCLI supports two automation approaches for ChatGPT.
|
|
4
|
+
|
|
5
|
+
## Approach 1: AppleScript (Default, No Setup)
|
|
6
|
+
|
|
7
|
+
The current built-in commands use native AppleScript automation — no extra launch flags needed.
|
|
8
|
+
|
|
9
|
+
### Prerequisites
|
|
10
|
+
1. Install the official [ChatGPT Desktop App](https://openai.com/chatgpt/mac/) from OpenAI.
|
|
11
|
+
2. Grant **Accessibility permissions** to your terminal app (Terminal / iTerm / Warp) in **System Settings → Privacy & Security → Accessibility**. This is required for System Events keystroke simulation.
|
|
12
|
+
|
|
13
|
+
### Commands
|
|
14
|
+
- `opencli chatgpt status`: Check if the ChatGPT app is currently running.
|
|
15
|
+
- `opencli chatgpt new`: Activate ChatGPT and press `Cmd+N` to start a new conversation.
|
|
16
|
+
- `opencli chatgpt send "message"`: Copy your message to clipboard, activate ChatGPT, paste, and submit.
|
|
17
|
+
- `opencli chatgpt read`: Copy the last AI response via `Cmd+Shift+C` and return it as text.
|
|
18
|
+
|
|
19
|
+
## Approach 2: CDP (Advanced, Electron Debug Mode)
|
|
20
|
+
|
|
21
|
+
ChatGPT Desktop is also an Electron app and can be launched with a remote debugging port for deeper automation via CDP:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
/Applications/ChatGPT.app/Contents/MacOS/ChatGPT \
|
|
25
|
+
--remote-debugging-port=9224
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then set the endpoint:
|
|
29
|
+
```bash
|
|
30
|
+
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> **Note**: The CDP approach enables future advanced commands like DOM inspection, model switching, and code extraction — similar to the Cursor and Codex adapters.
|
|
34
|
+
|
|
35
|
+
## How It Works
|
|
36
|
+
|
|
37
|
+
- **AppleScript mode**: Uses `osascript` and `pbcopy`/`pbpaste` for clipboard-based text transfer. No remote debugging port needed.
|
|
38
|
+
- **CDP mode**: Connects via Playwright to the Electron renderer process for direct DOM manipulation.
|
|
39
|
+
|
|
40
|
+
## Limitations
|
|
41
|
+
|
|
42
|
+
- macOS only (AppleScript dependency)
|
|
43
|
+
- AppleScript mode requires Accessibility permissions
|
|
44
|
+
- `read` command copies the last response — earlier messages need manual scroll
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# ChatGPT 桌面端适配器
|
|
2
|
+
|
|
3
|
+
在终端中直接控制 **ChatGPT macOS 桌面应用**。OpenCLI 支持两种自动化方式。
|
|
4
|
+
|
|
5
|
+
## 方式一:AppleScript(默认,无需配置)
|
|
6
|
+
|
|
7
|
+
内置命令使用原生 AppleScript 自动化,无需额外启动参数。
|
|
8
|
+
|
|
9
|
+
### 前置条件
|
|
10
|
+
1. 安装官方 [ChatGPT Desktop App](https://openai.com/chatgpt/mac/)。
|
|
11
|
+
2. 在 **系统设置 → 隐私与安全性 → 辅助功能** 中为终端应用授予权限。
|
|
12
|
+
|
|
13
|
+
### 命令
|
|
14
|
+
- `opencli chatgpt status`:检查 ChatGPT 应用是否在运行。
|
|
15
|
+
- `opencli chatgpt new`:激活 ChatGPT 并按 `Cmd+N` 开始新对话。
|
|
16
|
+
- `opencli chatgpt send "消息"`:将消息复制到剪贴板,激活 ChatGPT,粘贴并提交。
|
|
17
|
+
- `opencli chatgpt read`:通过 `Cmd+Shift+C` 复制最后一条 AI 回复并返回文本。
|
|
18
|
+
|
|
19
|
+
## 方式二:CDP(高级,Electron 调试模式)
|
|
20
|
+
|
|
21
|
+
ChatGPT Desktop 同样是 Electron 应用,可以通过远程调试端口启动以实现更深度的自动化:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
/Applications/ChatGPT.app/Contents/MacOS/ChatGPT \
|
|
25
|
+
--remote-debugging-port=9224
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
然后设置环境变量:
|
|
29
|
+
```bash
|
|
30
|
+
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> **注意**:CDP 模式支持未来的高级命令(如 DOM 检查、模型切换、代码提取等),与 Cursor 和 Codex 适配器类似。
|
|
34
|
+
|
|
35
|
+
## 工作原理
|
|
36
|
+
|
|
37
|
+
- **AppleScript 模式**:使用 `osascript` 和 `pbcopy`/`pbpaste` 进行剪贴板文本传输,无需远程调试端口。
|
|
38
|
+
- **CDP 模式**:通过 Playwright 连接到 Electron 渲染进程,直接操作 DOM。
|
|
39
|
+
|
|
40
|
+
## 限制
|
|
41
|
+
|
|
42
|
+
- 仅支持 macOS(AppleScript 依赖)
|
|
43
|
+
- AppleScript 模式需要辅助功能权限
|
|
44
|
+
- `read` 命令复制最后一条回复,更早的消息需手动滚动
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import type { IPage } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
export const askCommand = cli({
|
|
6
|
+
site: 'chatgpt',
|
|
7
|
+
name: 'ask',
|
|
8
|
+
description: 'Send a prompt and wait for the AI response (send + wait + read)',
|
|
9
|
+
domain: 'localhost',
|
|
10
|
+
strategy: Strategy.PUBLIC,
|
|
11
|
+
browser: false,
|
|
12
|
+
args: [
|
|
13
|
+
{ name: 'text', required: true, positional: true, help: 'Prompt to send' },
|
|
14
|
+
{ name: 'timeout', required: false, help: 'Max seconds to wait for response (default: 30)', default: '30' },
|
|
15
|
+
],
|
|
16
|
+
columns: ['Role', 'Text'],
|
|
17
|
+
func: async (page: IPage | null, kwargs: any) => {
|
|
18
|
+
const text = kwargs.text as string;
|
|
19
|
+
const timeout = parseInt(kwargs.timeout as string, 10) || 30;
|
|
20
|
+
|
|
21
|
+
// Backup clipboard
|
|
22
|
+
let clipBackup = '';
|
|
23
|
+
try { clipBackup = execSync('pbpaste', { encoding: 'utf-8' }); } catch {}
|
|
24
|
+
|
|
25
|
+
// Send the message
|
|
26
|
+
spawnSync('pbcopy', { input: text });
|
|
27
|
+
execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
|
|
28
|
+
execSync("osascript -e 'delay 0.5'");
|
|
29
|
+
|
|
30
|
+
const cmd = "osascript " +
|
|
31
|
+
"-e 'tell application \"System Events\"' " +
|
|
32
|
+
"-e 'keystroke \"v\" using command down' " +
|
|
33
|
+
"-e 'delay 0.2' " +
|
|
34
|
+
"-e 'keystroke return' " +
|
|
35
|
+
"-e 'end tell'";
|
|
36
|
+
execSync(cmd);
|
|
37
|
+
|
|
38
|
+
// Clear clipboard marker
|
|
39
|
+
spawnSync('pbcopy', { input: '__OPENCLI_WAITING__' });
|
|
40
|
+
|
|
41
|
+
// Wait for response, then read it
|
|
42
|
+
const pollInterval = 3;
|
|
43
|
+
const maxPolls = Math.ceil(timeout / pollInterval);
|
|
44
|
+
let response = '';
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
47
|
+
// Wait
|
|
48
|
+
execSync(`sleep ${pollInterval}`);
|
|
49
|
+
|
|
50
|
+
// Try Cmd+Shift+C to copy the latest response
|
|
51
|
+
execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
|
|
52
|
+
execSync("osascript -e 'tell application \"System Events\" to keystroke \"c\" using {command down, shift down}'");
|
|
53
|
+
execSync("osascript -e 'delay 0.3'");
|
|
54
|
+
|
|
55
|
+
const copied = execSync('pbpaste', { encoding: 'utf-8' }).trim();
|
|
56
|
+
if (copied && copied !== '__OPENCLI_WAITING__' && copied !== text) {
|
|
57
|
+
response = copied;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Restore clipboard
|
|
63
|
+
if (clipBackup) spawnSync('pbcopy', { input: clipBackup });
|
|
64
|
+
|
|
65
|
+
if (!response) {
|
|
66
|
+
return [
|
|
67
|
+
{ Role: 'User', Text: text },
|
|
68
|
+
{ Role: 'System', Text: `No response within ${timeout}s. ChatGPT may still be generating.` },
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
{ Role: 'User', Text: text },
|
|
74
|
+
{ Role: 'Assistant', Text: response },
|
|
75
|
+
];
|
|
76
|
+
},
|
|
77
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import type { IPage } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
export const newCommand = cli({
|
|
6
|
+
site: 'chatgpt',
|
|
7
|
+
name: 'new',
|
|
8
|
+
description: 'Open a new chat in ChatGPT Desktop App',
|
|
9
|
+
domain: 'localhost',
|
|
10
|
+
strategy: Strategy.PUBLIC,
|
|
11
|
+
browser: false,
|
|
12
|
+
args: [],
|
|
13
|
+
columns: ['Status'],
|
|
14
|
+
func: async (page: IPage | null) => {
|
|
15
|
+
try {
|
|
16
|
+
execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
|
|
17
|
+
execSync("osascript -e 'delay 0.5'");
|
|
18
|
+
execSync("osascript -e 'tell application \"System Events\" to keystroke \"n\" using command down'");
|
|
19
|
+
return [{ Status: 'Success' }];
|
|
20
|
+
} catch (err: any) {
|
|
21
|
+
return [{ Status: "Error: " + err.message }];
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import type { IPage } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
export const readCommand = cli({
|
|
6
|
+
site: 'chatgpt',
|
|
7
|
+
name: 'read',
|
|
8
|
+
description: 'Copy the most recent ChatGPT Desktop App response to clipboard and read it',
|
|
9
|
+
domain: 'localhost',
|
|
10
|
+
strategy: Strategy.PUBLIC,
|
|
11
|
+
browser: false,
|
|
12
|
+
args: [],
|
|
13
|
+
columns: ['Role', 'Text'],
|
|
14
|
+
func: async (page: IPage | null) => {
|
|
15
|
+
try {
|
|
16
|
+
execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
|
|
17
|
+
execSync("osascript -e 'delay 0.5'");
|
|
18
|
+
execSync("osascript -e 'tell application \"System Events\" to keystroke \"c\" using {command down, shift down}'");
|
|
19
|
+
execSync("osascript -e 'delay 0.3'");
|
|
20
|
+
|
|
21
|
+
const result = execSync('pbpaste', { encoding: 'utf-8' }).trim();
|
|
22
|
+
|
|
23
|
+
if (!result) {
|
|
24
|
+
return [{ Role: 'System', Text: 'No text was copied. Is there a response in the chat?' }];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return [{ Role: 'Assistant', Text: result }];
|
|
28
|
+
} catch (err: any) {
|
|
29
|
+
throw new Error("Failed to read from ChatGPT: " + err.message);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import type { IPage } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
export const sendCommand = cli({
|
|
6
|
+
site: 'chatgpt',
|
|
7
|
+
name: 'send',
|
|
8
|
+
description: 'Send a message to the active ChatGPT Desktop App window',
|
|
9
|
+
domain: 'localhost',
|
|
10
|
+
strategy: Strategy.PUBLIC,
|
|
11
|
+
browser: false,
|
|
12
|
+
args: [{ name: 'text', required: true, positional: true, help: 'Message to send' }],
|
|
13
|
+
columns: ['Status'],
|
|
14
|
+
func: async (page: IPage | null, kwargs: any) => {
|
|
15
|
+
const text = kwargs.text as string;
|
|
16
|
+
try {
|
|
17
|
+
// Backup current clipboard content
|
|
18
|
+
let clipBackup = '';
|
|
19
|
+
try {
|
|
20
|
+
clipBackup = execSync('pbpaste', { encoding: 'utf-8' });
|
|
21
|
+
} catch { /* clipboard may be empty */ }
|
|
22
|
+
|
|
23
|
+
// Copy text to clipboard
|
|
24
|
+
spawnSync('pbcopy', { input: text });
|
|
25
|
+
|
|
26
|
+
execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
|
|
27
|
+
execSync("osascript -e 'delay 0.5'");
|
|
28
|
+
|
|
29
|
+
const cmd = "osascript " +
|
|
30
|
+
"-e 'tell application \"System Events\"' " +
|
|
31
|
+
"-e 'keystroke \"v\" using command down' " +
|
|
32
|
+
"-e 'delay 0.2' " +
|
|
33
|
+
"-e 'keystroke return' " +
|
|
34
|
+
"-e 'end tell'";
|
|
35
|
+
|
|
36
|
+
execSync(cmd);
|
|
37
|
+
|
|
38
|
+
// Restore original clipboard content
|
|
39
|
+
if (clipBackup) {
|
|
40
|
+
spawnSync('pbcopy', { input: clipBackup });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [{ Status: 'Success' }];
|
|
44
|
+
} catch (err: any) {
|
|
45
|
+
return [{ Status: "Error: " + err.message }];
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import type { IPage } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
export const statusCommand = cli({
|
|
6
|
+
site: 'chatgpt',
|
|
7
|
+
name: 'status',
|
|
8
|
+
description: 'Check if ChatGPT Desktop App is running natively on macOS',
|
|
9
|
+
domain: 'localhost',
|
|
10
|
+
strategy: Strategy.PUBLIC,
|
|
11
|
+
browser: false,
|
|
12
|
+
args: [],
|
|
13
|
+
columns: ['Status'],
|
|
14
|
+
func: async (page: IPage | null) => {
|
|
15
|
+
try {
|
|
16
|
+
const output = execSync("osascript -e 'application \"ChatGPT\" is running'", { encoding: 'utf-8' }).trim();
|
|
17
|
+
return [{ Status: output === 'true' ? 'Running' : 'Stopped' }];
|
|
18
|
+
} catch {
|
|
19
|
+
return [{ Status: 'Error querying application state' }];
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
});
|
package/src/clis/codex/README.md
CHANGED
|
@@ -31,3 +31,4 @@ export OPENCLI_CODEX_CDP_ENDPOINT="http://127.0.0.1:9222"
|
|
|
31
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
32
|
- `opencli codex read`: Extracts the entire current thread history and AI reasoning logs into readable text.
|
|
33
33
|
- `opencli codex extract-diff`: Automatically scrapes any visual Patch chunks and Code Diffs the AI generated inside the review UI.
|
|
34
|
+
- `opencli codex model`: Get the currently active AI model.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
export const askCommand = cli({
|
|
5
|
+
site: 'codex',
|
|
6
|
+
name: 'ask',
|
|
7
|
+
description: 'Send a prompt and wait for the AI response (send + wait + read)',
|
|
8
|
+
domain: 'localhost',
|
|
9
|
+
strategy: Strategy.UI,
|
|
10
|
+
browser: true,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'text', required: true, positional: true, help: 'Prompt to send' },
|
|
13
|
+
{ name: 'timeout', required: false, help: 'Max seconds to wait for response (default: 60)', default: '60' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['Role', 'Text'],
|
|
16
|
+
func: async (page: IPage, kwargs: any) => {
|
|
17
|
+
const text = kwargs.text as string;
|
|
18
|
+
const timeout = parseInt(kwargs.timeout as string, 10) || 60;
|
|
19
|
+
|
|
20
|
+
// Snapshot the current content length before sending
|
|
21
|
+
const beforeLen = await page.evaluate(`
|
|
22
|
+
(function() {
|
|
23
|
+
const turns = document.querySelectorAll('[data-content-search-turn-key]');
|
|
24
|
+
return turns.length;
|
|
25
|
+
})()
|
|
26
|
+
`);
|
|
27
|
+
|
|
28
|
+
// Inject and send
|
|
29
|
+
await page.evaluate(`
|
|
30
|
+
(function(text) {
|
|
31
|
+
const editables = Array.from(document.querySelectorAll('[contenteditable="true"]'));
|
|
32
|
+
const composer = editables.length > 0 ? editables[editables.length - 1] : document.querySelector('textarea');
|
|
33
|
+
if (!composer) throw new Error('Could not find Codex input');
|
|
34
|
+
composer.focus();
|
|
35
|
+
document.execCommand('insertText', false, text);
|
|
36
|
+
})(${JSON.stringify(text)})
|
|
37
|
+
`);
|
|
38
|
+
await page.wait(0.5);
|
|
39
|
+
await page.pressKey('Enter');
|
|
40
|
+
|
|
41
|
+
// Poll for new content
|
|
42
|
+
const pollInterval = 3;
|
|
43
|
+
const maxPolls = Math.ceil(timeout / pollInterval);
|
|
44
|
+
let response = '';
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
47
|
+
await page.wait(pollInterval);
|
|
48
|
+
|
|
49
|
+
const result = await page.evaluate(`
|
|
50
|
+
(function(prevLen) {
|
|
51
|
+
const turns = document.querySelectorAll('[data-content-search-turn-key]');
|
|
52
|
+
if (turns.length <= prevLen) return null;
|
|
53
|
+
const lastTurn = turns[turns.length - 1];
|
|
54
|
+
const text = lastTurn.innerText || lastTurn.textContent;
|
|
55
|
+
return text ? text.trim() : null;
|
|
56
|
+
})(${beforeLen})
|
|
57
|
+
`);
|
|
58
|
+
|
|
59
|
+
if (result) {
|
|
60
|
+
response = result;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!response) {
|
|
66
|
+
return [
|
|
67
|
+
{ Role: 'User', Text: text },
|
|
68
|
+
{ Role: 'System', Text: `No response within ${timeout}s. The agent may still be working.` },
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
{ Role: 'User', Text: text },
|
|
74
|
+
{ Role: 'Assistant', Text: response },
|
|
75
|
+
];
|
|
76
|
+
},
|
|
77
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import type { IPage } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
export const exportCommand = cli({
|
|
6
|
+
site: 'codex',
|
|
7
|
+
name: 'export',
|
|
8
|
+
description: 'Export the current Codex conversation to a Markdown file',
|
|
9
|
+
domain: 'localhost',
|
|
10
|
+
strategy: Strategy.UI,
|
|
11
|
+
browser: true,
|
|
12
|
+
args: [
|
|
13
|
+
{ name: 'output', required: false, positional: true, help: 'Output file (default: /tmp/codex-export.md)' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['Status', 'File', 'Messages'],
|
|
16
|
+
func: async (page: IPage, kwargs: any) => {
|
|
17
|
+
const outputPath = (kwargs.output as string) || '/tmp/codex-export.md';
|
|
18
|
+
|
|
19
|
+
const md = await page.evaluate(`
|
|
20
|
+
(function() {
|
|
21
|
+
const turns = document.querySelectorAll('[data-content-search-turn-key]');
|
|
22
|
+
if (turns.length > 0) {
|
|
23
|
+
return Array.from(turns).map((t, i) => '## Turn ' + (i + 1) + '\\n\\n' + (t.innerText || t.textContent).trim()).join('\\n\\n---\\n\\n');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const main = document.querySelector('main, [role="main"], [role="log"]');
|
|
27
|
+
if (main) return main.innerText || main.textContent;
|
|
28
|
+
return document.body.innerText;
|
|
29
|
+
})()
|
|
30
|
+
`);
|
|
31
|
+
|
|
32
|
+
fs.writeFileSync(outputPath, '# Codex Conversation Export\\n\\n' + md);
|
|
33
|
+
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
Status: 'Success',
|
|
37
|
+
File: outputPath,
|
|
38
|
+
Messages: md.split('## Turn').length - 1,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
export const historyCommand = cli({
|
|
5
|
+
site: 'codex',
|
|
6
|
+
name: 'history',
|
|
7
|
+
description: 'List recent conversation threads in Codex',
|
|
8
|
+
domain: 'localhost',
|
|
9
|
+
strategy: Strategy.UI,
|
|
10
|
+
browser: true,
|
|
11
|
+
args: [],
|
|
12
|
+
columns: ['Index', 'Title'],
|
|
13
|
+
func: async (page: IPage) => {
|
|
14
|
+
const items = await page.evaluate(`
|
|
15
|
+
(function() {
|
|
16
|
+
const results = [];
|
|
17
|
+
// Codex thread list items
|
|
18
|
+
const entries = document.querySelectorAll('[data-testid*="thread"], [class*="thread-list"] a, [role="listbox"] [role="option"]');
|
|
19
|
+
|
|
20
|
+
entries.forEach((item, i) => {
|
|
21
|
+
const title = (item.textContent || item.innerText || '').trim().substring(0, 100);
|
|
22
|
+
if (title) results.push({ Index: i + 1, Title: title });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Fallback: sidebar/nav links
|
|
26
|
+
if (results.length === 0) {
|
|
27
|
+
const nav = document.querySelector('nav, [role="navigation"], aside');
|
|
28
|
+
if (nav) {
|
|
29
|
+
const links = nav.querySelectorAll('a, button');
|
|
30
|
+
links.forEach((link, i) => {
|
|
31
|
+
const text = (link.textContent || '').trim().substring(0, 100);
|
|
32
|
+
if (text && text.length > 3) results.push({ Index: i + 1, Title: text });
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return results;
|
|
38
|
+
})()
|
|
39
|
+
`);
|
|
40
|
+
|
|
41
|
+
if (items.length === 0) {
|
|
42
|
+
return [{ Index: 0, Title: 'No threads found. Try opening the thread list first.' }];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return items;
|
|
46
|
+
},
|
|
47
|
+
});
|
package/src/clis/codex/read.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
2
3
|
|
|
3
4
|
export const readCommand = cli({
|
|
4
5
|
site: 'codex',
|
|
@@ -7,31 +8,29 @@ export const readCommand = cli({
|
|
|
7
8
|
domain: 'localhost',
|
|
8
9
|
strategy: Strategy.UI,
|
|
9
10
|
browser: true,
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
args: [],
|
|
12
|
+
columns: ['Content'],
|
|
13
|
+
func: async (page: IPage) => {
|
|
12
14
|
const historyText = await page.evaluate(`
|
|
13
15
|
(function() {
|
|
14
|
-
// Precise Codex selector for chat messages
|
|
15
16
|
const turns = Array.from(document.querySelectorAll('[data-content-search-turn-key]'));
|
|
16
17
|
if (turns.length > 0) {
|
|
17
18
|
return turns.map(t => t.innerText || t.textContent).join('\\n\\n---\\n\\n');
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
// Fallback robust scraping heuristic for chat history panes
|
|
21
21
|
const threadContainer = document.querySelector('[role="log"], [data-testid="conversation"], .thread-container, .messages-list, main');
|
|
22
22
|
|
|
23
23
|
if (threadContainer) {
|
|
24
24
|
return threadContainer.innerText || threadContainer.textContent;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// If specific containers fail, just dump the whole body's readable text minus the navigation
|
|
28
27
|
return document.body.innerText;
|
|
29
28
|
})()
|
|
30
29
|
`);
|
|
31
30
|
|
|
32
31
|
return [
|
|
33
32
|
{
|
|
34
|
-
|
|
33
|
+
Content: historyText,
|
|
35
34
|
},
|
|
36
35
|
];
|
|
37
36
|
},
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import type { IPage } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
export const screenshotCommand = cli({
|
|
6
|
+
site: 'codex',
|
|
7
|
+
name: 'screenshot',
|
|
8
|
+
description: 'Capture a snapshot of the current Codex window (DOM + Accessibility tree)',
|
|
9
|
+
domain: 'localhost',
|
|
10
|
+
strategy: Strategy.UI,
|
|
11
|
+
browser: true,
|
|
12
|
+
args: [
|
|
13
|
+
{ name: 'output', required: false, positional: true, help: 'Output file path (default: /tmp/codex-snapshot.txt)' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['Status', 'File'],
|
|
16
|
+
func: async (page: IPage, kwargs: any) => {
|
|
17
|
+
const outputPath = (kwargs.output as string) || '/tmp/codex-snapshot.txt';
|
|
18
|
+
|
|
19
|
+
const snap = await page.snapshot({ compact: true });
|
|
20
|
+
const html = await page.evaluate('document.documentElement.outerHTML');
|
|
21
|
+
|
|
22
|
+
const htmlPath = outputPath.replace(/\.\w+$/, '') + '-dom.html';
|
|
23
|
+
const snapPath = outputPath.replace(/\.\w+$/, '') + '-a11y.txt';
|
|
24
|
+
|
|
25
|
+
fs.writeFileSync(htmlPath, html);
|
|
26
|
+
fs.writeFileSync(snapPath, typeof snap === 'string' ? snap : JSON.stringify(snap, null, 2));
|
|
27
|
+
|
|
28
|
+
return [
|
|
29
|
+
{ Status: 'Success', File: htmlPath },
|
|
30
|
+
{ Status: 'Success', File: snapPath },
|
|
31
|
+
];
|
|
32
|
+
},
|
|
33
|
+
});
|
package/src/clis/codex/send.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
2
3
|
|
|
3
4
|
export const sendCommand = cli({
|
|
4
5
|
site: 'codex',
|
|
@@ -9,19 +10,16 @@ export const sendCommand = cli({
|
|
|
9
10
|
browser: true,
|
|
10
11
|
args: [{ name: 'text', required: true, positional: true, help: 'Text, command (e.g. /review), or skill (e.g. $imagegen)' }],
|
|
11
12
|
columns: ['Status', 'InjectedText'],
|
|
12
|
-
func: async (page, kwargs) => {
|
|
13
|
+
func: async (page: IPage, kwargs: any) => {
|
|
13
14
|
const textToInsert = kwargs.text as string;
|
|
14
15
|
|
|
15
|
-
// We use evaluate to inject text bypassing complex nested shadow roots or contenteditables
|
|
16
16
|
await page.evaluate(`
|
|
17
17
|
(function(text) {
|
|
18
|
-
// Attempt 1: Look for standard textarea/composer input
|
|
19
18
|
let composer = document.querySelector('textarea, [contenteditable="true"]');
|
|
20
19
|
|
|
21
|
-
// Basic heuristic: prioritize elements that are deeply nested, visible, and have 'composer' or 'input' classes
|
|
22
20
|
const editables = Array.from(document.querySelectorAll('[contenteditable="true"]'));
|
|
23
21
|
if (editables.length > 0) {
|
|
24
|
-
composer = editables[editables.length - 1];
|
|
22
|
+
composer = editables[editables.length - 1];
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
if (!composer) {
|
|
@@ -29,12 +27,13 @@ export const sendCommand = cli({
|
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
composer.focus();
|
|
32
|
-
|
|
33
|
-
// This handles Lexical/ProseMirror/Monaco rich-text editors effectively by mimicking human paste/type deeply.
|
|
34
30
|
document.execCommand('insertText', false, text);
|
|
35
31
|
})(${JSON.stringify(textToInsert)})
|
|
36
32
|
`);
|
|
37
33
|
|
|
34
|
+
// Wait for the UI to register the input
|
|
35
|
+
await page.wait(0.5);
|
|
36
|
+
|
|
38
37
|
// Simulate Enter key to submit
|
|
39
38
|
await page.pressKey('Enter');
|
|
40
39
|
|
package/src/clis/codex/status.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
2
3
|
|
|
3
4
|
export const statusCommand = cli({
|
|
4
5
|
site: 'codex',
|
|
5
6
|
name: 'status',
|
|
6
7
|
description: 'Check active CDP connection to OpenAI Codex App',
|
|
7
8
|
domain: 'localhost',
|
|
8
|
-
strategy: Strategy.UI,
|
|
9
|
+
strategy: Strategy.UI,
|
|
9
10
|
browser: true,
|
|
11
|
+
args: [],
|
|
10
12
|
columns: ['Status', 'Url', 'Title'],
|
|
11
|
-
func: async (page) => {
|
|
13
|
+
func: async (page: IPage) => {
|
|
12
14
|
const url = await page.evaluate('window.location.href');
|
|
13
15
|
const title = await page.evaluate('document.title');
|
|
14
16
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Cursor Adapter for OpenCLI
|
|
2
|
+
|
|
3
|
+
Control the **Cursor IDE** from the terminal via Chrome DevTools Protocol (CDP). Since Cursor is built on Electron (VS Code fork), OpenCLI can drive its internal UI, automate Composer interactions, and manipulate chat sessions.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
1. Install [Cursor](https://cursor.sh/).
|
|
8
|
+
2. Launch it with the remote debugging port:
|
|
9
|
+
```bash
|
|
10
|
+
/Applications/Cursor.app/Contents/MacOS/Cursor --remote-debugging-port=9226
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9226"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
### Diagnostics
|
|
22
|
+
- `opencli cursor status`: Check CDP connection status.
|
|
23
|
+
- `opencli cursor dump`: Dump the full DOM and Accessibility snapshot to `/tmp/cursor-dom.html` and `/tmp/cursor-snapshot.json`.
|
|
24
|
+
|
|
25
|
+
### Chat Manipulation
|
|
26
|
+
- `opencli cursor new`: Press `Cmd+N` to start a new file/tab.
|
|
27
|
+
- `opencli cursor send "message"`: Inject text into the active Composer/Chat input and submit.
|
|
28
|
+
- `opencli cursor read`: Extract the full conversation history from the active chat panel.
|
|
29
|
+
|
|
30
|
+
### AI Features
|
|
31
|
+
- `opencli cursor composer "prompt"`: Open the Composer panel (`Cmd+I`) and send a prompt for inline AI editing.
|
|
32
|
+
- `opencli cursor model`: Get the currently active AI model (e.g., `claude-4.5-sonnet`).
|
|
33
|
+
- `opencli cursor extract-code`: Extract all code blocks from the current conversation.
|