@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,35 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const extractCodeCommand = cli({
3
+ site: 'cursor',
4
+ name: 'extract-code',
5
+ description: 'Extract multi-line code blocks from the current Cursor conversation',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [],
10
+ columns: ['Code'],
11
+ func: async (page) => {
12
+ const blocks = await page.evaluate(`
13
+ (function() {
14
+ // Find standard pre/code blocks
15
+ let elements = Array.from(document.querySelectorAll('pre code, .markdown-root pre'));
16
+
17
+ // Fallback to Monaco editor content inside the UI
18
+ if (elements.length === 0) {
19
+ elements = Array.from(document.querySelectorAll('.monaco-editor'));
20
+ }
21
+
22
+ // Generic fallback to any code tag that spans multiple lines
23
+ if (elements.length === 0) {
24
+ elements = Array.from(document.querySelectorAll('code')).filter(c => c.innerText.includes('\\n'));
25
+ }
26
+
27
+ return elements.map(el => el.innerText || el.textContent || '').filter(text => text.trim().length > 0);
28
+ })()
29
+ `);
30
+ if (!blocks || blocks.length === 0) {
31
+ return [{ Code: 'No code blocks found in Cursor.' }];
32
+ }
33
+ return blocks.map((code) => ({ Code: code }));
34
+ },
35
+ });
@@ -0,0 +1 @@
1
+ export declare const modelCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,53 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const modelCommand = cli({
3
+ site: 'cursor',
4
+ name: 'model',
5
+ description: 'Get or switch the currently active AI model in Cursor',
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. claude-3.5-sonnet)' }
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
17
+ const currentModel = await page.evaluate(`
18
+ (function() {
19
+ const m = document.querySelector('.composer-unified-dropdown-model span, [class*="unifiedmodeldropdown"] span');
20
+ return m ? m.textContent.trim() : 'Unknown or Not Found';
21
+ })()
22
+ `);
23
+ return [
24
+ {
25
+ Status: 'Active',
26
+ Model: currentModel,
27
+ },
28
+ ];
29
+ }
30
+ else {
31
+ // Try to switch model (click dropdown, type/select model)
32
+ const success = await page.evaluate(`
33
+ (function(targetModel) {
34
+ const dropdown = document.querySelector('.composer-unified-dropdown-model, [class*="unifiedmodeldropdown"]');
35
+ if (!dropdown) return 'Dropdown not found';
36
+
37
+ dropdown.click();
38
+ // After clicking, the DOM usually spawns a popup list.
39
+ // Because it's hard to predict exactly how the list renders,
40
+ // a simple scriptable approach is just to click it, and hope we can select it via UI.
41
+ // In many React apps, clicking it opens a menu, and clicking the item works.
42
+ return 'Dropdown opened. Automated switching is not fully generic. Please implement precise list navigation depending on DOM.';
43
+ })(${JSON.stringify(desiredModel)})
44
+ `);
45
+ return [
46
+ {
47
+ Status: success,
48
+ Model: desiredModel,
49
+ },
50
+ ];
51
+ }
52
+ },
53
+ });
@@ -0,0 +1 @@
1
+ export declare const newCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,27 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const newCommand = cli({
3
+ site: 'cursor',
4
+ name: 'new',
5
+ description: 'Start a new Cursor chat or Composer session',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ columns: ['Status'],
10
+ func: async (page) => {
11
+ const success = await page.evaluate(`
12
+ (function() {
13
+ const newChatButton = document.querySelector('[aria-label="New Chat"], [aria-label="New Chat (⌘N)"], .agent-sidebar-new-agent-button');
14
+ if (newChatButton) {
15
+ newChatButton.click();
16
+ return true;
17
+ }
18
+ return false;
19
+ })()
20
+ `);
21
+ if (!success) {
22
+ throw new Error('Could not find New Chat button in Cursor DOM.');
23
+ }
24
+ await page.wait(1);
25
+ return [{ Status: 'Success' }];
26
+ },
27
+ });
@@ -0,0 +1 @@
1
+ export declare const readCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,43 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const readCommand = cli({
3
+ site: 'cursor',
4
+ name: 'read',
5
+ description: 'Read the current Cursor chat/composer conversation history',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ columns: ['Role', 'Text'],
10
+ func: async (page) => {
11
+ const history = await page.evaluate(`
12
+ (function() {
13
+ const messages = Array.from(document.querySelectorAll('[data-message-role]'));
14
+
15
+ if (messages.length === 0) {
16
+ return [];
17
+ }
18
+
19
+ return messages.map(msg => {
20
+ const role = msg.getAttribute('data-message-role');
21
+ let text = '';
22
+
23
+ // Try to get structured markdown root for AI, or lexical text for human
24
+ const markdownRoot = msg.querySelector('.markdown-root');
25
+ if (markdownRoot) {
26
+ text = markdownRoot.innerText || markdownRoot.textContent;
27
+ } else {
28
+ text = msg.innerText || msg.textContent;
29
+ }
30
+
31
+ return {
32
+ Role: role === 'human' ? 'User' : 'Assistant',
33
+ Text: text.trim()
34
+ };
35
+ });
36
+ })()
37
+ `);
38
+ if (!history || history.length === 0) {
39
+ throw new Error('No conversation history found in Cursor.');
40
+ }
41
+ return history;
42
+ },
43
+ });
@@ -0,0 +1 @@
1
+ export declare const sendCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,39 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const sendCommand = cli({
3
+ site: 'cursor',
4
+ name: 'send',
5
+ description: 'Send a prompt directly into Cursor Composer/Chat',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [{ name: 'text', required: true, positional: true, help: 'Text to send into Cursor' }],
10
+ columns: ['Status', 'InjectedText'],
11
+ func: async (page, kwargs) => {
12
+ const textToInsert = kwargs.text;
13
+ const injected = await page.evaluate(`(function(text) {
14
+ // Find the Lexical editor input for Composer or Chat
15
+ let composer = document.querySelector('.aislash-editor-input, [data-lexical-editor="true"], [contenteditable="true"]');
16
+
17
+ if (!composer) {
18
+ return false;
19
+ }
20
+
21
+ composer.focus();
22
+ document.execCommand('insertText', false, text);
23
+ return true;
24
+ })(${JSON.stringify(textToInsert)})`);
25
+ if (!injected) {
26
+ throw new Error('Could not find Cursor Composer input element.');
27
+ }
28
+ // Submit the command. In Cursor, Enter usually submits the chat.
29
+ await page.wait(0.5);
30
+ await page.pressKey('Enter');
31
+ await page.wait(1);
32
+ return [
33
+ {
34
+ Status: 'Success',
35
+ InjectedText: textToInsert,
36
+ },
37
+ ];
38
+ },
39
+ });
@@ -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: 'cursor',
4
+ name: 'status',
5
+ description: 'Check active CDP connection to Cursor AI Editor',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "0.9.2",
3
+ "version": "0.9.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -0,0 +1,35 @@
1
+ # ChatGPT Desktop Adapter for OpenCLI
2
+
3
+ Control the **ChatGPT macOS Desktop App** directly from the terminal via native AppleScript automation. Unlike Electron-based apps (Antigravity, Codex, Cursor), ChatGPT Desktop is a native macOS application — OpenCLI drives it using `osascript` and System Events.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. Install the official [ChatGPT Desktop App](https://openai.com/chatgpt/mac/) from OpenAI.
8
+ 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.
9
+
10
+ ## Setup
11
+
12
+ No extra environment variables needed — the adapter uses `osascript` directly.
13
+
14
+ ## Commands
15
+
16
+ ### Diagnostics
17
+ - `opencli chatgpt status`: Check if the ChatGPT app is currently running.
18
+
19
+ ### Chat Manipulation
20
+ - `opencli chatgpt new`: Activate ChatGPT and press `Cmd+N` to start a new conversation.
21
+ - `opencli chatgpt send "message"`: Copy your message to clipboard, activate ChatGPT, paste, and submit.
22
+ - `opencli chatgpt read`: Copy the last AI response via `Cmd+Shift+C` and return it as text.
23
+
24
+ ## How It Works
25
+
26
+ Unlike CDP-based adapters, this adapter:
27
+ - Uses `osascript` to send AppleScript commands to System Events
28
+ - Leverages `pbcopy`/`pbpaste` for clipboard-based text transfer
29
+ - Requires no remote debugging port — works with the stock app
30
+
31
+ ## Limitations
32
+
33
+ - macOS only (AppleScript dependency)
34
+ - Requires Accessibility permissions for keystroke simulation
35
+ - `read` command copies the last response — earlier messages need manual scroll
@@ -0,0 +1,35 @@
1
+ # ChatGPT 桌面端适配器
2
+
3
+ 通过原生 AppleScript 自动化,在终端中直接控制 **ChatGPT macOS 桌面应用**。与基于 Electron 的应用(Antigravity、Codex、Cursor)不同,ChatGPT Desktop 是原生 macOS 应用 — OpenCLI 使用 `osascript` 和 System Events 来驱动它。
4
+
5
+ ## 前置条件
6
+
7
+ 1. 安装官方 [ChatGPT Desktop App](https://openai.com/chatgpt/mac/)。
8
+ 2. 在 **系统设置 → 隐私与安全性 → 辅助功能** 中为终端应用(Terminal / iTerm / Warp)授予 **辅助功能权限**。这是 System Events 按键模拟所必需的。
9
+
10
+ ## 配置
11
+
12
+ 无需额外环境变量 — 适配器直接使用 `osascript`。
13
+
14
+ ## 命令
15
+
16
+ ### 诊断
17
+ - `opencli chatgpt status`:检查 ChatGPT 应用是否在运行。
18
+
19
+ ### 对话操作
20
+ - `opencli chatgpt new`:激活 ChatGPT 并按 `Cmd+N` 开始新对话。
21
+ - `opencli chatgpt send "消息"`:将消息复制到剪贴板,激活 ChatGPT,粘贴并提交。
22
+ - `opencli chatgpt read`:通过 `Cmd+Shift+C` 复制最后一条 AI 回复并返回文本。
23
+
24
+ ## 工作原理
25
+
26
+ 与基于 CDP 的适配器不同,此适配器:
27
+ - 使用 `osascript` 向 System Events 发送 AppleScript 命令
28
+ - 利用 `pbcopy`/`pbpaste` 进行基于剪贴板的文本传输
29
+ - 无需远程调试端口 — 直接与原生应用交互
30
+
31
+ ## 限制
32
+
33
+ - 仅支持 macOS(AppleScript 依赖)
34
+ - 需要辅助功能权限以模拟按键
35
+ - `read` 命令复制最后一条回复 — 更早的消息需要手动滚动
@@ -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,36 @@
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
+ spawnSync('pbcopy', { input: text });
18
+
19
+ execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
20
+ execSync("osascript -e 'delay 0.5'");
21
+
22
+ const cmd = "osascript " +
23
+ "-e 'tell application \"System Events\"' " +
24
+ "-e 'keystroke \"v\" using command down' " +
25
+ "-e 'delay 0.2' " +
26
+ "-e 'keystroke return' " +
27
+ "-e 'end tell'";
28
+
29
+ execSync(cmd);
30
+
31
+ return [{ Status: 'Success' }];
32
+ } catch (err: any) {
33
+ return [{ Status: "Error: " + err.message }];
34
+ }
35
+ },
36
+ });
@@ -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
+ });
@@ -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,59 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const modelCommand = cli({
5
+ site: 'codex',
6
+ name: 'model',
7
+ description: 'Get or switch the currently active AI model in Codex Desktop',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [
12
+ { name: 'model_name', required: false, positional: true, help: 'The ID of the model to switch to (e.g. gpt-4)' }
13
+ ],
14
+ columns: ['Status', 'Model'],
15
+ func: async (page: IPage, kwargs: any) => {
16
+ const desiredModel = kwargs.model_name as string | undefined;
17
+
18
+ if (!desiredModel) {
19
+ // Just read the current model. We traverse iframes/webviews if needed.
20
+ const currentModel = await page.evaluate(`
21
+ (function() {
22
+ // Look for any typical model switcher selectors in the DOM
23
+ let m = document.querySelector('[title*="Model"], [aria-label*="Model"], .model-selector, [class*="ModelPicker"]');
24
+
25
+ if (!m && document.querySelector('webview, iframe')) {
26
+ // Not directly in main DOM, might be in a webview, but Playwright evaluate doesn't cross origin boundaries easily without frames[].
27
+ return 'Unknown (Likely inside a WebView, please focus the Chat tab)';
28
+ }
29
+ return m ? (m.textContent || m.getAttribute('title') || m.getAttribute('aria-label')).trim() : 'Unknown or Not Found';
30
+ })()
31
+ `);
32
+
33
+ return [
34
+ {
35
+ Status: 'Active',
36
+ Model: currentModel,
37
+ },
38
+ ];
39
+ } else {
40
+ // Try to switch model (click dropdown, type/select model)
41
+ const success = await page.evaluate(`
42
+ (function(targetModel) {
43
+ const dropdown = document.querySelector('[title*="Model"], [aria-label*="Model"], .model-selector, [class*="ModelPicker"]');
44
+ if (!dropdown) return 'Dropdown not found';
45
+
46
+ dropdown.click();
47
+ return 'Dropdown clicked. Generic interaction initiated.';
48
+ })(${JSON.stringify(desiredModel)})
49
+ `);
50
+
51
+ return [
52
+ {
53
+ Status: success,
54
+ Model: desiredModel,
55
+ },
56
+ ];
57
+ }
58
+ },
59
+ });
@@ -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.
@@ -0,0 +1,33 @@
1
+ # Cursor 适配器
2
+
3
+ 通过 Chrome DevTools Protocol (CDP) 在终端中控制 **Cursor IDE**。由于 Cursor 基于 Electron(VS Code 分支),OpenCLI 可以驱动其内部 UI,自动化 Composer 交互,操控聊天会话。
4
+
5
+ ## 前置条件
6
+
7
+ 1. 安装 [Cursor](https://cursor.sh/)。
8
+ 2. 通过远程调试端口启动:
9
+ ```bash
10
+ /Applications/Cursor.app/Contents/MacOS/Cursor --remote-debugging-port=9226
11
+ ```
12
+
13
+ ## 配置
14
+
15
+ ```bash
16
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9226"
17
+ ```
18
+
19
+ ## 命令
20
+
21
+ ### 诊断
22
+ - `opencli cursor status`:检查 CDP 连接状态。
23
+ - `opencli cursor dump`:导出完整 DOM 和 Accessibility 快照到 `/tmp/cursor-dom.html` 和 `/tmp/cursor-snapshot.json`。
24
+
25
+ ### 对话操作
26
+ - `opencli cursor new`:按 `Cmd+N` 创建新文件/标签。
27
+ - `opencli cursor send "消息"`:将文本注入活跃的 Composer/Chat 输入框并提交。
28
+ - `opencli cursor read`:提取当前聊天面板的完整对话历史。
29
+
30
+ ### AI 功能
31
+ - `opencli cursor composer "提示词"`:打开 Composer 面板(`Cmd+I`)并发送提示词进行内联 AI 编辑。
32
+ - `opencli cursor model`:获取当前活跃的 AI 模型(如 `claude-4.5-sonnet`)。
33
+ - `opencli cursor extract-code`:从当前对话中提取所有代码块。
@@ -0,0 +1,71 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const composerCommand = cli({
5
+ site: 'cursor',
6
+ name: 'composer',
7
+ description: 'Send a prompt directly into Cursor Composer (Cmd+I shortcut)',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [{ name: 'text', required: true, positional: true, help: 'Text to send into Composer' }],
12
+ columns: ['Status', 'InjectedText'],
13
+ func: async (page: IPage, kwargs: any) => {
14
+ const textToInsert = kwargs.text as string;
15
+
16
+ const injected = await page.evaluate(
17
+ `(async function() {
18
+ let isComposerVisible = document.querySelector('.composer-bar') !== null || document.querySelector('#composer-toolbar-section') !== null;
19
+ return isComposerVisible;
20
+ })()`
21
+ );
22
+
23
+ if (!injected) {
24
+ await page.pressKey('Meta+I');
25
+ await page.wait(1.0);
26
+ } else {
27
+ // Just focus it if it's open but unfocused (we can't easily know if it's focused without triggering something)
28
+ await page.pressKey('Meta+I');
29
+ await page.wait(0.2);
30
+ const isStillVisible = await page.evaluate('document.querySelector(".composer-bar") !== null');
31
+ if (!isStillVisible) {
32
+ await page.pressKey('Meta+I'); // Re-open
33
+ await page.wait(0.5);
34
+ }
35
+ }
36
+
37
+ const typed = await page.evaluate(
38
+ `(function(text) {
39
+ let composer = document.querySelector('.composer-bar [data-lexical-editor="true"], [id*="composer"] [contenteditable="true"], .aislash-editor-input');
40
+
41
+ if (!composer) {
42
+ composer = document.activeElement;
43
+ if (!composer || !composer.isContentEditable) {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ composer.focus();
49
+ document.execCommand('insertText', false, text);
50
+ return true;
51
+ })(${JSON.stringify(textToInsert)})`
52
+ );
53
+
54
+ if (!typed) {
55
+ throw new Error('Could not find Cursor Composer input element after pressing Cmd+I.');
56
+ }
57
+
58
+ // Submit the command. In Cursor Composer, Enter usually submits if it's not a multi-line edit.
59
+ // Sometimes Cmd+Enter is needed? We'll just submit standard Enter.
60
+ await page.wait(0.5);
61
+ await page.pressKey('Enter');
62
+ await page.wait(1);
63
+
64
+ return [
65
+ {
66
+ Status: 'Success (Composer)',
67
+ InjectedText: textToInsert,
68
+ },
69
+ ];
70
+ },
71
+ });