@jackwener/opencli 0.9.2 → 0.9.5

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 (49) hide show
  1. package/CLI-ELECTRON.md +72 -0
  2. package/README.md +5 -1
  3. package/README.zh-CN.md +5 -1
  4. package/dist/cli-manifest.json +231 -0
  5. package/dist/clis/chatgpt/new.d.ts +1 -0
  6. package/dist/clis/chatgpt/new.js +23 -0
  7. package/dist/clis/chatgpt/read.d.ts +1 -0
  8. package/dist/clis/chatgpt/read.js +28 -0
  9. package/dist/clis/chatgpt/send.d.ts +1 -0
  10. package/dist/clis/chatgpt/send.js +31 -0
  11. package/dist/clis/chatgpt/status.d.ts +1 -0
  12. package/dist/clis/chatgpt/status.js +21 -0
  13. package/dist/clis/codex/model.d.ts +1 -0
  14. package/dist/clis/codex/model.js +55 -0
  15. package/dist/clis/cursor/composer.d.ts +1 -0
  16. package/dist/clis/cursor/composer.js +60 -0
  17. package/dist/clis/cursor/dump.d.ts +1 -0
  18. package/dist/clis/cursor/dump.js +25 -0
  19. package/dist/clis/cursor/extract-code.d.ts +1 -0
  20. package/dist/clis/cursor/extract-code.js +35 -0
  21. package/dist/clis/cursor/model.d.ts +1 -0
  22. package/dist/clis/cursor/model.js +53 -0
  23. package/dist/clis/cursor/new.d.ts +1 -0
  24. package/dist/clis/cursor/new.js +27 -0
  25. package/dist/clis/cursor/read.d.ts +1 -0
  26. package/dist/clis/cursor/read.js +43 -0
  27. package/dist/clis/cursor/send.d.ts +1 -0
  28. package/dist/clis/cursor/send.js +39 -0
  29. package/dist/clis/cursor/status.d.ts +1 -0
  30. package/dist/clis/cursor/status.js +21 -0
  31. package/package.json +1 -1
  32. package/src/clis/chatgpt/README.md +35 -0
  33. package/src/clis/chatgpt/README.zh-CN.md +35 -0
  34. package/src/clis/chatgpt/new.ts +24 -0
  35. package/src/clis/chatgpt/read.ts +32 -0
  36. package/src/clis/chatgpt/send.ts +36 -0
  37. package/src/clis/chatgpt/status.ts +22 -0
  38. package/src/clis/codex/README.md +1 -0
  39. package/src/clis/codex/model.ts +59 -0
  40. package/src/clis/cursor/README.md +33 -0
  41. package/src/clis/cursor/README.zh-CN.md +33 -0
  42. package/src/clis/cursor/composer.ts +71 -0
  43. package/src/clis/cursor/dump.ts +28 -0
  44. package/src/clis/cursor/extract-code.ts +39 -0
  45. package/src/clis/cursor/model.ts +57 -0
  46. package/src/clis/cursor/new.ts +32 -0
  47. package/src/clis/cursor/read.ts +47 -0
  48. package/src/clis/cursor/send.ts +47 -0
  49. package/src/clis/cursor/status.ts +23 -0
@@ -0,0 +1,72 @@
1
+ ---
2
+ description: How to CLI-ify and automate any Electron Desktop Application via CDP
3
+ ---
4
+
5
+ # CLI-ifying Electron Applications (Skill Guide)
6
+
7
+ Based on the successful extraction and automation of **Antigravity** and **OpenAI Codex** desktop apps, this guide serves as the standard operating procedure (SOP) for adapting ANY Electron-based application into an OpenCLI adapter.
8
+
9
+ ## 核心原理 (Core Concept)
10
+ Electron 应用本质上是运行在本地的 Chromium 浏览器实例。只要在启动应用时暴露了调试端口(CDP,Chrome DevTools Protocol),我们就可以利用 Playwright MCP 直接穿透其 UI 层,获取并操控包括 React/Vue 组件、Shadow DOM 等在内的所有底层状态,实现“从应用外挂入自动化脚本”。
11
+
12
+ ### 启动 Target App
13
+ 要在本地操作任何 Electron 应用,必须先要求用户使用以下参数注入调试端点:
14
+ ```bash
15
+ /Applications/AppName.app/Contents/MacOS/AppName --remote-debugging-port=9222
16
+ ```
17
+
18
+ ## 标准适配模式:The 5-Command Pattern
19
+
20
+ 适配一个新的 App,必须在 `src/clis/<app_name>/` 下实现这 5 个标准化指令:
21
+
22
+ ### 1. `status.ts` (连接测试)
23
+ 负责确认应用监听正确。
24
+ - **机制**: 直接 `export const statusCommand = cli({...})`
25
+ - **核心代码**: 获取 `window.location.href` 与 `document.title`。
26
+ - **注意**: 必须指明 `domain: 'localhost'` 和 `browser: true`。
27
+
28
+ ### 2. `dump.ts` (逆向工程核心)
29
+ 很多现代 App DOM 极其庞大且混淆。**千万不要直接猜选择器**。
30
+ 首先编写 dump 脚本,将当前页面的 DOM 与 Accessibility Tree 导出到 `/tmp/`,方便用 AI (或者 `grep`) 提取精确的容器名称和 Class。
31
+ ```typescript
32
+ const dom = await page.evaluate('document.body.innerHTML');
33
+ fs.writeFileSync('/tmp/app-dom.html', dom);
34
+ const snap = await page.snapshot({ interactive: false });
35
+ fs.writeFileSync('/tmp/app-snapshot.json', JSON.stringify(snap, null, 2));
36
+ ```
37
+
38
+ ### 3. `send.ts` (高级注入技巧)
39
+ Electron 应用常常使用极端复杂的富文本编辑器(如 Monaco, Lexical, ProseMirror)。直接修改元素的 `value` 常常会被 React 状态机忽略。
40
+ - **最佳实践**: 使用 `document.execCommand('insertText')` 完美模拟真实的人类复制粘贴输入流,完全穿透 React state。
41
+ ```javascript
42
+ // 寻路机制:优先尝试寻找 contenteditable
43
+ let composer = document.querySelector('[contenteditable="true"]');
44
+ composer.focus();
45
+ document.execCommand('insertText', false, "你好");
46
+ ```
47
+ - **提交快捷键**: `await page.pressKey('Enter')`。
48
+
49
+ ### 4. `read.ts` (上下文解析)
50
+ 不要提取整个页面的文本。应该利用 `dump.ts` 抓取出来的特征寻找真正的“对话容器”。
51
+ - **技巧**: 检查带有语义化结构的数据,例如 `[role="log"]`、`[data-testid="conversation"]` 或是 `[data-content-search-turn-key]`。
52
+ - **格式化**: 拼接抓取出的文本转粗暴渲染成 Markdown 返回,这样不仅你和人类能读懂,LLM 后续作为 Agent 也能精准切分。
53
+
54
+ ### 5. `new.ts` / Action Macros (底层事件模拟)
55
+ 许多图形界面操作难以找到按钮实例,但它们通常响应原生快捷键。
56
+ - **最佳实践**: 模拟系统级快捷键直接驱动 `(Meta+N / Control+N)`。
57
+ ```typescript
58
+ const isMac = process.platform === 'darwin';
59
+ await page.pressKey(isMac ? 'Meta+N' : 'Control+N');
60
+ await page.wait(1); // 等待重渲染
61
+ ```
62
+
63
+ ## 全局环境变量
64
+ 为了让 Core Framework 挂载到我们指定的端口,必须在执行指令前(或在 README 中指导用户)注入目标环境端口:
65
+ ```bash
66
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9222"
67
+ ```
68
+
69
+ ## 踩坑避雷 (Pitfalls & Gotchas)
70
+ 1. **端口占用 (EADDRINUSE)**: 确保同一时间只能有一个 App 占据一个端口。如果同时测试 Antigravity (9224) 且你要测试别的 App (9222),要将 CDP Endpoint 分配开来。
71
+ 2. **TypeScript 抽象**: OpenCLI 内部封装了 `IPage` 类型(`src/types.ts`),不是原生的 Playwright Page。要用 `page.pressKey()` 和 `page.evaluate()`,而非 `page.keyboard.press()`。
72
+ 3. **延时等待**: DOM 发生剧烈变化后,一定要加上 `await page.wait(0.5)` 到 `1.0` 给框架反应的时间。不要立刻 return 导致连接 prematurely 阻断。
package/README.md CHANGED
@@ -143,16 +143,20 @@ npm install -g @jackwener/opencli@latest
143
143
 
144
144
  ## Built-in Commands
145
145
 
146
- **19 sites · 80+ commands** — run `opencli list` for the live registry.
146
+ **26 sites · 119 commands** — run `opencli list` for the live registry.
147
147
 
148
148
  | Site | Commands | Count | Mode |
149
149
  |------|----------|:-----:|------|
150
150
  | **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `thread` `following` `followers` `notifications` `post` `reply` `delete` `like` `article` `follow` `unfollow` `bookmark` `unbookmark` | 18 | 🔐 Browser |
151
151
  | **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `subscribe` `saved` `upvoted` | 15 | 🔐 Browser |
152
152
  | **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` | 11 | 🔐 Browser |
153
+ | **cursor** | `status` `send` `read` `new` `dump` `composer` `model` `extract-code` | 8 | 🖥️ Desktop |
154
+ | **codex** | `status` `send` `read` `new` `extract-diff` `model` | 6 | 🖥️ Desktop |
153
155
  | **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | 6 | 🌐 / 🔐 |
154
156
  | **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 6 | 🔐 Browser |
157
+ | **antigravity** | `status` `send` `read` `new` `evaluate` | 5 | 🖥️ Desktop |
155
158
  | **xiaohongshu** | `search` `notifications` `feed` `me` `user` | 5 | 🔐 Browser |
159
+ | **chatgpt** | `status` `new` `send` `read` | 4 | 🖥️ Desktop |
156
160
  | **xiaoyuzhou** | `podcast` `podcast-episodes` `episode` | 3 | 🌐 Public |
157
161
  | **youtube** | `search` `video` `transcript` | 3 | 🔐 Browser |
158
162
  | **zhihu** | `hot` `search` `question` | 3 | 🔐 Browser |
package/README.zh-CN.md CHANGED
@@ -144,12 +144,16 @@ npm install -g @jackwener/opencli@latest
144
144
 
145
145
  ## 内置命令
146
146
 
147
- **19 个站点 · 80+ 命令** — 运行 `opencli list` 查看完整注册表。
147
+ **26 个站点 · 119 命令** — 运行 `opencli list` 查看完整注册表。
148
148
 
149
149
  | 站点 | 命令 | 数量 | 模式 |
150
150
  |------|------|:----:|------|
151
151
  | **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `thread` `following` `followers` `notifications` `post` `reply` `delete` `like` `article` `follow` `unfollow` `bookmark` `unbookmark` | 18 | 🔐 浏览器 |
152
152
  | **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `subscribe` `saved` `upvoted` | 15 | 🔐 浏览器 |
153
+ | **antigravity** | `status` `send` `read` `new` `evaluate` | 5 | 🖥️ 桌面端 |
154
+ | **chatgpt** | `status` `new` `send` `read` | 4 | 🖥️ 桌面端 |
155
+ | **codex** | `status` `send` `read` `new` `extract-diff` `model` | 6 | 🖥️ 桌面端 |
156
+ | **cursor** | `status` `send` `read` `new` `dump` `composer` `model` `extract-code` | 8 | 🖥️ 桌面端 |
153
157
  | **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` | 11 | 🔐 浏览器 |
154
158
  | **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | 6 | 🌐 / 🔐 |
155
159
  | **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 6 | 🔐 浏览器 |
@@ -810,6 +810,71 @@
810
810
  "url"
811
811
  ]
812
812
  },
813
+ {
814
+ "site": "chatgpt",
815
+ "name": "new",
816
+ "description": "Open a new chat in ChatGPT Desktop App",
817
+ "strategy": "public",
818
+ "browser": false,
819
+ "args": [],
820
+ "type": "ts",
821
+ "modulePath": "chatgpt/new.js",
822
+ "domain": "localhost",
823
+ "columns": [
824
+ "Status"
825
+ ]
826
+ },
827
+ {
828
+ "site": "chatgpt",
829
+ "name": "read",
830
+ "description": "Copy the most recent ChatGPT Desktop App response to clipboard and read it",
831
+ "strategy": "public",
832
+ "browser": false,
833
+ "args": [],
834
+ "type": "ts",
835
+ "modulePath": "chatgpt/read.js",
836
+ "domain": "localhost",
837
+ "columns": [
838
+ "Role",
839
+ "Text"
840
+ ]
841
+ },
842
+ {
843
+ "site": "chatgpt",
844
+ "name": "send",
845
+ "description": "Send a message to the active ChatGPT Desktop App window",
846
+ "strategy": "public",
847
+ "browser": false,
848
+ "args": [
849
+ {
850
+ "name": "text",
851
+ "type": "str",
852
+ "required": true,
853
+ "positional": true,
854
+ "help": "Message to send"
855
+ }
856
+ ],
857
+ "type": "ts",
858
+ "modulePath": "chatgpt/send.js",
859
+ "domain": "localhost",
860
+ "columns": [
861
+ "Status"
862
+ ]
863
+ },
864
+ {
865
+ "site": "chatgpt",
866
+ "name": "status",
867
+ "description": "Check if ChatGPT Desktop App is running natively on macOS",
868
+ "strategy": "public",
869
+ "browser": false,
870
+ "args": [],
871
+ "type": "ts",
872
+ "modulePath": "chatgpt/status.js",
873
+ "domain": "localhost",
874
+ "columns": [
875
+ "Status"
876
+ ]
877
+ },
813
878
  {
814
879
  "site": "codex",
815
880
  "name": "dump",
@@ -840,6 +905,29 @@
840
905
  "Diff"
841
906
  ]
842
907
  },
908
+ {
909
+ "site": "codex",
910
+ "name": "model",
911
+ "description": "Get or switch the currently active AI model in Codex Desktop",
912
+ "strategy": "ui",
913
+ "browser": true,
914
+ "args": [
915
+ {
916
+ "name": "model_name",
917
+ "type": "str",
918
+ "required": false,
919
+ "positional": true,
920
+ "help": "The ID of the model to switch to (e.g. gpt-4)"
921
+ }
922
+ ],
923
+ "type": "ts",
924
+ "modulePath": "codex/model.js",
925
+ "domain": "localhost",
926
+ "columns": [
927
+ "Status",
928
+ "Model"
929
+ ]
930
+ },
843
931
  {
844
932
  "site": "codex",
845
933
  "name": "new",
@@ -1021,6 +1109,149 @@
1021
1109
  "url"
1022
1110
  ]
1023
1111
  },
1112
+ {
1113
+ "site": "cursor",
1114
+ "name": "composer",
1115
+ "description": "Send a prompt directly into Cursor Composer (Cmd+I shortcut)",
1116
+ "strategy": "ui",
1117
+ "browser": true,
1118
+ "args": [
1119
+ {
1120
+ "name": "text",
1121
+ "type": "str",
1122
+ "required": true,
1123
+ "positional": true,
1124
+ "help": "Text to send into Composer"
1125
+ }
1126
+ ],
1127
+ "type": "ts",
1128
+ "modulePath": "cursor/composer.js",
1129
+ "domain": "localhost",
1130
+ "columns": [
1131
+ "Status",
1132
+ "InjectedText"
1133
+ ]
1134
+ },
1135
+ {
1136
+ "site": "cursor",
1137
+ "name": "dump",
1138
+ "description": "Dump the DOM and Accessibility tree of Cursor for reverse-engineering",
1139
+ "strategy": "ui",
1140
+ "browser": true,
1141
+ "args": [],
1142
+ "type": "ts",
1143
+ "modulePath": "cursor/dump.js",
1144
+ "domain": "localhost",
1145
+ "columns": [
1146
+ "action",
1147
+ "files"
1148
+ ]
1149
+ },
1150
+ {
1151
+ "site": "cursor",
1152
+ "name": "extract-code",
1153
+ "description": "Extract multi-line code blocks from the current Cursor conversation",
1154
+ "strategy": "ui",
1155
+ "browser": true,
1156
+ "args": [],
1157
+ "type": "ts",
1158
+ "modulePath": "cursor/extract-code.js",
1159
+ "domain": "localhost",
1160
+ "columns": [
1161
+ "Code"
1162
+ ]
1163
+ },
1164
+ {
1165
+ "site": "cursor",
1166
+ "name": "model",
1167
+ "description": "Get or switch the currently active AI model in Cursor",
1168
+ "strategy": "ui",
1169
+ "browser": true,
1170
+ "args": [
1171
+ {
1172
+ "name": "model_name",
1173
+ "type": "str",
1174
+ "required": false,
1175
+ "positional": true,
1176
+ "help": "The ID of the model to switch to (e.g. claude-3.5-sonnet)"
1177
+ }
1178
+ ],
1179
+ "type": "ts",
1180
+ "modulePath": "cursor/model.js",
1181
+ "domain": "localhost",
1182
+ "columns": [
1183
+ "Status",
1184
+ "Model"
1185
+ ]
1186
+ },
1187
+ {
1188
+ "site": "cursor",
1189
+ "name": "new",
1190
+ "description": "Start a new Cursor chat or Composer session",
1191
+ "strategy": "ui",
1192
+ "browser": true,
1193
+ "args": [],
1194
+ "type": "ts",
1195
+ "modulePath": "cursor/new.js",
1196
+ "domain": "localhost",
1197
+ "columns": [
1198
+ "Status"
1199
+ ]
1200
+ },
1201
+ {
1202
+ "site": "cursor",
1203
+ "name": "read",
1204
+ "description": "Read the current Cursor chat/composer conversation history",
1205
+ "strategy": "ui",
1206
+ "browser": true,
1207
+ "args": [],
1208
+ "type": "ts",
1209
+ "modulePath": "cursor/read.js",
1210
+ "domain": "localhost",
1211
+ "columns": [
1212
+ "Role",
1213
+ "Text"
1214
+ ]
1215
+ },
1216
+ {
1217
+ "site": "cursor",
1218
+ "name": "send",
1219
+ "description": "Send a prompt directly into Cursor Composer/Chat",
1220
+ "strategy": "ui",
1221
+ "browser": true,
1222
+ "args": [
1223
+ {
1224
+ "name": "text",
1225
+ "type": "str",
1226
+ "required": true,
1227
+ "positional": true,
1228
+ "help": "Text to send into Cursor"
1229
+ }
1230
+ ],
1231
+ "type": "ts",
1232
+ "modulePath": "cursor/send.js",
1233
+ "domain": "localhost",
1234
+ "columns": [
1235
+ "Status",
1236
+ "InjectedText"
1237
+ ]
1238
+ },
1239
+ {
1240
+ "site": "cursor",
1241
+ "name": "status",
1242
+ "description": "Check active CDP connection to Cursor AI Editor",
1243
+ "strategy": "ui",
1244
+ "browser": true,
1245
+ "args": [],
1246
+ "type": "ts",
1247
+ "modulePath": "cursor/status.js",
1248
+ "domain": "localhost",
1249
+ "columns": [
1250
+ "Status",
1251
+ "Url",
1252
+ "Title"
1253
+ ]
1254
+ },
1024
1255
  {
1025
1256
  "site": "hackernews",
1026
1257
  "name": "top",
@@ -0,0 +1 @@
1
+ export declare const newCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,23 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ export const newCommand = cli({
4
+ site: 'chatgpt',
5
+ name: 'new',
6
+ description: 'Open a new chat in ChatGPT Desktop App',
7
+ domain: 'localhost',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [],
11
+ columns: ['Status'],
12
+ func: async (page) => {
13
+ try {
14
+ execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
15
+ execSync("osascript -e 'delay 0.5'");
16
+ execSync("osascript -e 'tell application \"System Events\" to keystroke \"n\" using command down'");
17
+ return [{ Status: 'Success' }];
18
+ }
19
+ catch (err) {
20
+ return [{ Status: "Error: " + err.message }];
21
+ }
22
+ },
23
+ });
@@ -0,0 +1 @@
1
+ export declare const readCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,28 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ export const readCommand = cli({
4
+ site: 'chatgpt',
5
+ name: 'read',
6
+ description: 'Copy the most recent ChatGPT Desktop App response to clipboard and read it',
7
+ domain: 'localhost',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [],
11
+ columns: ['Role', 'Text'],
12
+ func: async (page) => {
13
+ try {
14
+ execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
15
+ execSync("osascript -e 'delay 0.5'");
16
+ execSync("osascript -e 'tell application \"System Events\" to keystroke \"c\" using {command down, shift down}'");
17
+ execSync("osascript -e 'delay 0.3'");
18
+ const result = execSync('pbpaste', { encoding: 'utf-8' }).trim();
19
+ if (!result) {
20
+ return [{ Role: 'System', Text: 'No text was copied. Is there a response in the chat?' }];
21
+ }
22
+ return [{ Role: 'Assistant', Text: result }];
23
+ }
24
+ catch (err) {
25
+ throw new Error("Failed to read from ChatGPT: " + err.message);
26
+ }
27
+ },
28
+ });
@@ -0,0 +1 @@
1
+ export declare const sendCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,31 @@
1
+ import { execSync, spawnSync } from 'node:child_process';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ export const sendCommand = cli({
4
+ site: 'chatgpt',
5
+ name: 'send',
6
+ description: 'Send a message to the active ChatGPT Desktop App window',
7
+ domain: 'localhost',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [{ name: 'text', required: true, positional: true, help: 'Message to send' }],
11
+ columns: ['Status'],
12
+ func: async (page, kwargs) => {
13
+ const text = kwargs.text;
14
+ try {
15
+ spawnSync('pbcopy', { input: text });
16
+ execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
17
+ execSync("osascript -e 'delay 0.5'");
18
+ const cmd = "osascript " +
19
+ "-e 'tell application \"System Events\"' " +
20
+ "-e 'keystroke \"v\" using command down' " +
21
+ "-e 'delay 0.2' " +
22
+ "-e 'keystroke return' " +
23
+ "-e 'end tell'";
24
+ execSync(cmd);
25
+ return [{ Status: 'Success' }];
26
+ }
27
+ catch (err) {
28
+ return [{ Status: "Error: " + err.message }];
29
+ }
30
+ },
31
+ });
@@ -0,0 +1 @@
1
+ export declare const statusCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,21 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ export const statusCommand = cli({
4
+ site: 'chatgpt',
5
+ name: 'status',
6
+ description: 'Check if ChatGPT Desktop App is running natively on macOS',
7
+ domain: 'localhost',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [],
11
+ columns: ['Status'],
12
+ func: async (page) => {
13
+ try {
14
+ const output = execSync("osascript -e 'application \"ChatGPT\" is running'", { encoding: 'utf-8' }).trim();
15
+ return [{ Status: output === 'true' ? 'Running' : 'Stopped' }];
16
+ }
17
+ catch {
18
+ return [{ Status: 'Error querying application state' }];
19
+ }
20
+ },
21
+ });
@@ -0,0 +1 @@
1
+ export declare const modelCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,55 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const modelCommand = cli({
3
+ site: 'codex',
4
+ name: 'model',
5
+ description: 'Get or switch the currently active AI model in Codex Desktop',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [
10
+ { name: 'model_name', required: false, positional: true, help: 'The ID of the model to switch to (e.g. gpt-4)' }
11
+ ],
12
+ columns: ['Status', 'Model'],
13
+ func: async (page, kwargs) => {
14
+ const desiredModel = kwargs.model_name;
15
+ if (!desiredModel) {
16
+ // Just read the current model. We traverse iframes/webviews if needed.
17
+ const currentModel = await page.evaluate(`
18
+ (function() {
19
+ // Look for any typical model switcher selectors in the DOM
20
+ let m = document.querySelector('[title*="Model"], [aria-label*="Model"], .model-selector, [class*="ModelPicker"]');
21
+
22
+ if (!m && document.querySelector('webview, iframe')) {
23
+ // Not directly in main DOM, might be in a webview, but Playwright evaluate doesn't cross origin boundaries easily without frames[].
24
+ return 'Unknown (Likely inside a WebView, please focus the Chat tab)';
25
+ }
26
+ return m ? (m.textContent || m.getAttribute('title') || m.getAttribute('aria-label')).trim() : 'Unknown or Not Found';
27
+ })()
28
+ `);
29
+ return [
30
+ {
31
+ Status: 'Active',
32
+ Model: currentModel,
33
+ },
34
+ ];
35
+ }
36
+ else {
37
+ // Try to switch model (click dropdown, type/select model)
38
+ const success = await page.evaluate(`
39
+ (function(targetModel) {
40
+ const dropdown = document.querySelector('[title*="Model"], [aria-label*="Model"], .model-selector, [class*="ModelPicker"]');
41
+ if (!dropdown) return 'Dropdown not found';
42
+
43
+ dropdown.click();
44
+ return 'Dropdown clicked. Generic interaction initiated.';
45
+ })(${JSON.stringify(desiredModel)})
46
+ `);
47
+ return [
48
+ {
49
+ Status: success,
50
+ Model: desiredModel,
51
+ },
52
+ ];
53
+ }
54
+ },
55
+ });
@@ -0,0 +1 @@
1
+ export declare const composerCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,60 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const composerCommand = cli({
3
+ site: 'cursor',
4
+ name: 'composer',
5
+ description: 'Send a prompt directly into Cursor Composer (Cmd+I shortcut)',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [{ name: 'text', required: true, positional: true, help: 'Text to send into Composer' }],
10
+ columns: ['Status', 'InjectedText'],
11
+ func: async (page, kwargs) => {
12
+ const textToInsert = kwargs.text;
13
+ const injected = await page.evaluate(`(async function() {
14
+ let isComposerVisible = document.querySelector('.composer-bar') !== null || document.querySelector('#composer-toolbar-section') !== null;
15
+ return isComposerVisible;
16
+ })()`);
17
+ if (!injected) {
18
+ await page.pressKey('Meta+I');
19
+ await page.wait(1.0);
20
+ }
21
+ else {
22
+ // Just focus it if it's open but unfocused (we can't easily know if it's focused without triggering something)
23
+ await page.pressKey('Meta+I');
24
+ await page.wait(0.2);
25
+ const isStillVisible = await page.evaluate('document.querySelector(".composer-bar") !== null');
26
+ if (!isStillVisible) {
27
+ await page.pressKey('Meta+I'); // Re-open
28
+ await page.wait(0.5);
29
+ }
30
+ }
31
+ const typed = await page.evaluate(`(function(text) {
32
+ let composer = document.querySelector('.composer-bar [data-lexical-editor="true"], [id*="composer"] [contenteditable="true"], .aislash-editor-input');
33
+
34
+ if (!composer) {
35
+ composer = document.activeElement;
36
+ if (!composer || !composer.isContentEditable) {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ composer.focus();
42
+ document.execCommand('insertText', false, text);
43
+ return true;
44
+ })(${JSON.stringify(textToInsert)})`);
45
+ if (!typed) {
46
+ throw new Error('Could not find Cursor Composer input element after pressing Cmd+I.');
47
+ }
48
+ // Submit the command. In Cursor Composer, Enter usually submits if it's not a multi-line edit.
49
+ // Sometimes Cmd+Enter is needed? We'll just submit standard Enter.
50
+ await page.wait(0.5);
51
+ await page.pressKey('Enter');
52
+ await page.wait(1);
53
+ return [
54
+ {
55
+ Status: 'Success (Composer)',
56
+ InjectedText: textToInsert,
57
+ },
58
+ ];
59
+ },
60
+ });
@@ -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: 'cursor',
5
+ name: 'dump',
6
+ description: 'Dump the DOM and Accessibility tree of Cursor 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/cursor-dom.html', dom);
15
+ // Get accessibility snapshot
16
+ const snap = await page.snapshot({ interactive: false });
17
+ fs.writeFileSync('/tmp/cursor-snapshot.json', JSON.stringify(snap, null, 2));
18
+ return [
19
+ {
20
+ action: 'Dom extraction finished',
21
+ files: '/tmp/cursor-dom.html, /tmp/cursor-snapshot.json',
22
+ },
23
+ ];
24
+ },
25
+ });
@@ -0,0 +1 @@
1
+ export declare const extractCodeCommand: import("../../registry.js").CliCommand;