@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,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,81 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
export const askCommand = cli({
|
|
5
|
+
site: 'cursor',
|
|
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: 30)', default: '30' },
|
|
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) || 30;
|
|
19
|
+
|
|
20
|
+
// Count existing messages before sending
|
|
21
|
+
const beforeCount = await page.evaluate(`
|
|
22
|
+
document.querySelectorAll('[data-message-role]').length
|
|
23
|
+
`);
|
|
24
|
+
|
|
25
|
+
// Inject text into the active editor and submit
|
|
26
|
+
const injected = await page.evaluate(
|
|
27
|
+
`(function(text) {
|
|
28
|
+
let editor = document.querySelector('.aislash-editor-input, [data-lexical-editor="true"], [contenteditable="true"]');
|
|
29
|
+
if (!editor) return false;
|
|
30
|
+
editor.focus();
|
|
31
|
+
document.execCommand('insertText', false, text);
|
|
32
|
+
return true;
|
|
33
|
+
})(${JSON.stringify(text)})`
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (!injected) throw new Error('Could not find input element.');
|
|
37
|
+
await page.wait(0.5);
|
|
38
|
+
await page.pressKey('Enter');
|
|
39
|
+
|
|
40
|
+
// Poll until a new assistant message appears or timeout
|
|
41
|
+
const pollInterval = 2; // seconds
|
|
42
|
+
const maxPolls = Math.ceil(timeout / pollInterval);
|
|
43
|
+
let response = '';
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
46
|
+
await page.wait(pollInterval);
|
|
47
|
+
|
|
48
|
+
const result = await page.evaluate(`
|
|
49
|
+
(function(prevCount) {
|
|
50
|
+
const msgs = document.querySelectorAll('[data-message-role]');
|
|
51
|
+
if (msgs.length <= prevCount) return null;
|
|
52
|
+
|
|
53
|
+
const lastMsg = msgs[msgs.length - 1];
|
|
54
|
+
const role = lastMsg.getAttribute('data-message-role');
|
|
55
|
+
if (role === 'human') return null; // Still waiting for assistant
|
|
56
|
+
|
|
57
|
+
const root = lastMsg.querySelector('.markdown-root');
|
|
58
|
+
const text = root ? root.innerText : lastMsg.innerText;
|
|
59
|
+
return text ? text.trim() : null;
|
|
60
|
+
})(${beforeCount})
|
|
61
|
+
`);
|
|
62
|
+
|
|
63
|
+
if (result) {
|
|
64
|
+
response = result;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!response) {
|
|
70
|
+
return [
|
|
71
|
+
{ Role: 'User', Text: text },
|
|
72
|
+
{ Role: 'System', Text: `No response received within ${timeout}s. The AI may still be generating.` },
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return [
|
|
77
|
+
{ Role: 'User', Text: text },
|
|
78
|
+
{ Role: 'Assistant', Text: response },
|
|
79
|
+
];
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -13,37 +13,18 @@ export const composerCommand = cli({
|
|
|
13
13
|
func: async (page: IPage, kwargs: any) => {
|
|
14
14
|
const textToInsert = kwargs.text as string;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
}
|
|
16
|
+
// Open/Focus Composer via shortcut — always works regardless of current state
|
|
17
|
+
await page.pressKey('Meta+I');
|
|
18
|
+
await page.wait(1);
|
|
36
19
|
|
|
37
20
|
const typed = await page.evaluate(
|
|
38
21
|
`(function(text) {
|
|
39
|
-
let composer = document.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
composer = document.activeElement;
|
|
43
|
-
if (!composer || !composer.isContentEditable) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
22
|
+
let composer = document.activeElement;
|
|
23
|
+
if (!composer || !composer.isContentEditable) {
|
|
24
|
+
composer = document.querySelector('.composer-bar [data-lexical-editor="true"], [id*="composer"] [contenteditable="true"], .aislash-editor-input');
|
|
46
25
|
}
|
|
26
|
+
|
|
27
|
+
if (!composer) return false;
|
|
47
28
|
|
|
48
29
|
composer.focus();
|
|
49
30
|
document.execCommand('insertText', false, text);
|
|
@@ -55,15 +36,13 @@ export const composerCommand = cli({
|
|
|
55
36
|
throw new Error('Could not find Cursor Composer input element after pressing Cmd+I.');
|
|
56
37
|
}
|
|
57
38
|
|
|
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
39
|
await page.wait(0.5);
|
|
61
40
|
await page.pressKey('Enter');
|
|
62
41
|
await page.wait(1);
|
|
63
42
|
|
|
64
43
|
return [
|
|
65
44
|
{
|
|
66
|
-
Status: 'Success
|
|
45
|
+
Status: 'Success',
|
|
67
46
|
InjectedText: textToInsert,
|
|
68
47
|
},
|
|
69
48
|
];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import type { IPage } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
function makeExportCommand(site: string, readSelector: string) {
|
|
6
|
+
return cli({
|
|
7
|
+
site,
|
|
8
|
+
name: 'export',
|
|
9
|
+
description: `Export the current ${site} conversation to a Markdown file`,
|
|
10
|
+
domain: 'localhost',
|
|
11
|
+
strategy: Strategy.UI,
|
|
12
|
+
browser: true,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'output', required: false, positional: true, help: `Output file (default: /tmp/${site}-export.md)` },
|
|
15
|
+
],
|
|
16
|
+
columns: ['Status', 'File', 'Messages'],
|
|
17
|
+
func: async (page: IPage, kwargs: any) => {
|
|
18
|
+
const outputPath = (kwargs.output as string) || `/tmp/${site}-export.md`;
|
|
19
|
+
|
|
20
|
+
const md = await page.evaluate(`
|
|
21
|
+
(function() {
|
|
22
|
+
const selectors = ${JSON.stringify(readSelector)}.split(',');
|
|
23
|
+
let messages = [];
|
|
24
|
+
|
|
25
|
+
for (const sel of selectors) {
|
|
26
|
+
const nodes = document.querySelectorAll(sel.trim());
|
|
27
|
+
if (nodes.length > 0) {
|
|
28
|
+
messages = Array.from(nodes).map(n => n.innerText || n.textContent);
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (messages.length === 0) {
|
|
34
|
+
const main = document.querySelector('main, [role="main"], .messages-list, [role="log"]');
|
|
35
|
+
if (main) messages = [main.innerText || main.textContent];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (messages.length === 0) messages = [document.body.innerText];
|
|
39
|
+
|
|
40
|
+
return messages.map((m, i) => '## Message ' + (i + 1) + '\\n\\n' + m.trim()).join('\\n\\n---\\n\\n');
|
|
41
|
+
})()
|
|
42
|
+
`);
|
|
43
|
+
|
|
44
|
+
fs.writeFileSync(outputPath, `# ${site} Conversation Export\\n\\n` + md);
|
|
45
|
+
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
Status: 'Success',
|
|
49
|
+
File: outputPath,
|
|
50
|
+
Messages: md.split('## Message').length - 1,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const cursorExport = makeExportCommand('cursor', '[data-message-role]');
|
|
@@ -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: 'cursor',
|
|
6
|
+
name: 'history',
|
|
7
|
+
description: 'List recent chat sessions from the Cursor sidebar',
|
|
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
|
+
// Cursor chat history lives in sidebar items
|
|
18
|
+
const entries = document.querySelectorAll('.agent-sidebar-list-item, [data-testid="chat-history-item"], .chat-history-item, .tree-item');
|
|
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: try to find sidebar text items
|
|
26
|
+
if (results.length === 0) {
|
|
27
|
+
const sidebar = document.querySelector('.sidebar, [class*="sidebar"], .agent-sidebar, .side-bar-container');
|
|
28
|
+
if (sidebar) {
|
|
29
|
+
const links = sidebar.querySelectorAll('a, [role="treeitem"], [role="option"]');
|
|
30
|
+
links.forEach((link, i) => {
|
|
31
|
+
const text = (link.textContent || '').trim().substring(0, 100);
|
|
32
|
+
if (text) 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 chat history found. Open the AI sidebar first.' }];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return items;
|
|
46
|
+
},
|
|
47
|
+
});
|
package/src/clis/cursor/new.ts
CHANGED
|
@@ -8,23 +8,12 @@ export const newCommand = cli({
|
|
|
8
8
|
domain: 'localhost',
|
|
9
9
|
strategy: Strategy.UI,
|
|
10
10
|
browser: true,
|
|
11
|
+
args: [],
|
|
11
12
|
columns: ['Status'],
|
|
12
13
|
func: async (page: IPage) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (newChatButton) {
|
|
17
|
-
newChatButton.click();
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
return false;
|
|
21
|
-
})()
|
|
22
|
-
`);
|
|
23
|
-
|
|
24
|
-
if (!success) {
|
|
25
|
-
throw new Error('Could not find New Chat button in Cursor DOM.');
|
|
26
|
-
}
|
|
27
|
-
|
|
14
|
+
// Use keyboard shortcut — most robust approach, avoids brittle DOM selectors
|
|
15
|
+
const isMac = process.platform === 'darwin';
|
|
16
|
+
await page.pressKey(isMac ? 'Meta+N' : 'Control+N');
|
|
28
17
|
await page.wait(1);
|
|
29
18
|
|
|
30
19
|
return [{ Status: 'Success' }];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import type { IPage } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
function makeScreenshotCommand(site: string) {
|
|
6
|
+
return cli({
|
|
7
|
+
site,
|
|
8
|
+
name: 'screenshot',
|
|
9
|
+
description: `Capture a snapshot of the current ${site} window (DOM + Accessibility tree)`,
|
|
10
|
+
domain: 'localhost',
|
|
11
|
+
strategy: Strategy.UI,
|
|
12
|
+
browser: true,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'output', required: false, positional: true, help: `Output file path (default: /tmp/${site}-snapshot.txt)` },
|
|
15
|
+
],
|
|
16
|
+
columns: ['Status', 'File'],
|
|
17
|
+
func: async (page: IPage, kwargs: any) => {
|
|
18
|
+
const outputPath = (kwargs.output as string) || `/tmp/${site}-snapshot.txt`;
|
|
19
|
+
|
|
20
|
+
// Get both the accessibility snapshot and the raw DOM HTML
|
|
21
|
+
const snap = await page.snapshot({ compact: true });
|
|
22
|
+
const html = await page.evaluate('document.documentElement.outerHTML');
|
|
23
|
+
|
|
24
|
+
const htmlPath = outputPath.replace(/\.\w+$/, '') + '-dom.html';
|
|
25
|
+
const snapPath = outputPath.replace(/\.\w+$/, '') + '-a11y.txt';
|
|
26
|
+
|
|
27
|
+
fs.writeFileSync(htmlPath, html);
|
|
28
|
+
fs.writeFileSync(snapPath, typeof snap === 'string' ? snap : JSON.stringify(snap, null, 2));
|
|
29
|
+
|
|
30
|
+
return [
|
|
31
|
+
{ Status: 'Success', File: htmlPath },
|
|
32
|
+
{ Status: 'Success', File: snapPath },
|
|
33
|
+
];
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const screenshotCursor = makeScreenshotCommand('cursor');
|