feat: add advanced cursor and codex capabilities

This commit is contained in:
jackwener committed 2026-03-18 17:41:41 +08:00
1 parent 981cc1bc5e
commit b32fe1cbc3
13 files changed
+418 -3

No files matched your search

+9
View File
@@ -149,7 +149,16 @@ npm install -g @jackwener/opencli@latest
|------|----------|:-----:|------|
| **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `thread` `following` `followers` `notifications` `post` `reply` `delete` `like` `article` `follow` `unfollow` `bookmark` `unbookmark` | 18 | 🔐 Browser |
| **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `subscribe` `saved` `upvoted` | 15 | 🔐 Browser |
| **antigravity** | `status` `send` `read` `new` `evaluate` | 5 | 🖥️ Desktop |
| **bbc** | `news` | 1 | 🌐 Public |
| **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` | 11 | 🔐 Browser |
| **boss** | `search` `detail` | 2 | 🔐 Browser |
| **codex** | `status` `send` `read` `new` `extract-diff` `model` | 6 | 🖥️ Desktop |
| **coupang** | `search` `add-to-cart` | 2 | 🔐 Browser |
| **ctrip** | `search` | 1 | 🔐 Browser |
| **cursor** | `status` `send` `read` `new` `dump` `composer` `model` | 7 | 🖥️ Desktop |
| **github** | `search` | 1 | 🌐 Public |
| **hackernews** | `top` | 1 | 🌐 Public |
| **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | 6 | 🌐 / 🔐 |
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 6 | 🔐 Browser |
| **xiaohongshu** | `search` `notifications` `feed` `me` `user` | 5 | 🔐 Browser |
+3
View File
@@ -150,6 +150,9 @@ npm install -g @jackwener/opencli@latest
|------|------|:----:|------|
| **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `thread` `following` `followers` `notifications` `post` `reply` `delete` `like` `article` `follow` `unfollow` `bookmark` `unbookmark` | 18 | 🔐 浏览器 |
| **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `subscribe` `saved` `upvoted` | 15 | 🔐 浏览器 |
| **antigravity** | `status` `send` `read` `new` `evaluate` | 5 | 🖥️ 桌面端 |
| **codex** | `status` `send` `read` `new` `extract-diff` `model` | 6 | 🖥️ 桌面端 |
| **cursor** | `status` `send` `read` `new` `dump` `composer` `model` | 7 | 🖥️ 桌面端 |
| **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` | 11 | 🔐 浏览器 |
| **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | 6 | 🌐 / 🔐 |
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 6 | 🔐 浏览器 |
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@jackwener/opencli",
"version": "0.9.2",
"version": "0.9.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@jackwener/opencli",
"version": "0.9.2",
"version": "0.9.3",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@jackwener/opencli",
"version": "0.9.2",
"version": "0.9.3",
"publishConfig": {
"access": "public"
},
+59
View File
@@ -0,0 +1,59 @@
import { cli, Strategy } from '../../registry.js';
import type { IPage } from '../../types.js';
export const modelCommand = cli({
site: 'codex',
name: 'model',
description: 'Get or switch the currently active AI model in Codex Desktop',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [
{ name: 'model_name', required: false, positional: true, help: 'The ID of the model to switch to (e.g. gpt-4)' }
],
columns: ['Status', 'Model'],
func: async (page: IPage, kwargs: any) => {
const desiredModel = kwargs.model_name as string | undefined;
if (!desiredModel) {
// Just read the current model. We traverse iframes/webviews if needed.
const currentModel = await page.evaluate(`
(function() {
// Look for any typical model switcher selectors in the DOM
let m = document.querySelector('[title*="Model"], [aria-label*="Model"], .model-selector, [class*="ModelPicker"]');
if (!m && document.querySelector('webview, iframe')) {
// Not directly in main DOM, might be in a webview, but Playwright evaluate doesn't cross origin boundaries easily without frames[].
return 'Unknown (Likely inside a WebView, please focus the Chat tab)';
}
return m ? (m.textContent || m.getAttribute('title') || m.getAttribute('aria-label')).trim() : 'Unknown or Not Found';
})()
`);
return [
{
Status: 'Active',
Model: currentModel,
},
];
} else {
// Try to switch model (click dropdown, type/select model)
const success = await page.evaluate(`
(function(targetModel) {
const dropdown = document.querySelector('[title*="Model"], [aria-label*="Model"], .model-selector, [class*="ModelPicker"]');
if (!dropdown) return 'Dropdown not found';
dropdown.click();
return 'Dropdown clicked. Generic interaction initiated.';
})(${JSON.stringify(desiredModel)})
`);
return [
{
Status: success,
Model: desiredModel,
},
];
}
},
});
+71
View File
@@ -0,0 +1,71 @@
import { cli, Strategy } from '../../registry.js';
import type { IPage } from '../../types.js';
export const composerCommand = cli({
site: 'cursor',
name: 'composer',
description: 'Send a prompt directly into Cursor Composer (Cmd+I shortcut)',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [{ name: 'text', required: true, positional: true, help: 'Text to send into Composer' }],
columns: ['Status', 'InjectedText'],
func: async (page: IPage, kwargs: any) => {
const textToInsert = kwargs.text as string;
const injected = await page.evaluate(
`(async function() {
let isComposerVisible = document.querySelector('.composer-bar') !== null || document.querySelector('#composer-toolbar-section') !== null;
return isComposerVisible;
})()`
);
if (!injected) {
await page.pressKey('Meta+I');
await page.wait(1.0);
} else {
// Just focus it if it's open but unfocused (we can't easily know if it's focused without triggering something)
await page.pressKey('Meta+I');
await page.wait(0.2);
const isStillVisible = await page.evaluate('document.querySelector(".composer-bar") !== null');
if (!isStillVisible) {
await page.pressKey('Meta+I'); // Re-open
await page.wait(0.5);
}
}
const typed = await page.evaluate(
`(function(text) {
let composer = document.querySelector('.composer-bar [data-lexical-editor="true"], [id*="composer"] [contenteditable="true"], .aislash-editor-input');
if (!composer) {
composer = document.activeElement;
if (!composer || !composer.isContentEditable) {
return false;
}
}
composer.focus();
document.execCommand('insertText', false, text);
return true;
})(${JSON.stringify(textToInsert)})`
);
if (!typed) {
throw new Error('Could not find Cursor Composer input element after pressing Cmd+I.');
}
// Submit the command. In Cursor Composer, Enter usually submits if it's not a multi-line edit.
// Sometimes Cmd+Enter is needed? We'll just submit standard Enter.
await page.wait(0.5);
await page.pressKey('Enter');
await page.wait(1);
return [
{
Status: 'Success (Composer)',
InjectedText: textToInsert,
},
];
},
});
+28
View File
@@ -0,0 +1,28 @@
import { cli, Strategy } from '../../registry.js';
import * as fs from 'fs';
export const dumpCommand = cli({
site: 'cursor',
name: 'dump',
description: 'Dump the DOM and Accessibility tree of Cursor for reverse-engineering',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
columns: ['action', 'files'],
func: async (page) => {
// Extract full HTML
const dom = await page.evaluate('document.body.innerHTML');
fs.writeFileSync('/tmp/cursor-dom.html', dom);
// Get accessibility snapshot
const snap = await page.snapshot({ interactive: false });
fs.writeFileSync('/tmp/cursor-snapshot.json', JSON.stringify(snap, null, 2));
return [
{
action: 'Dom extraction finished',
files: '/tmp/cursor-dom.html, /tmp/cursor-snapshot.json',
},
];
},
});
+39
View File
@@ -0,0 +1,39 @@
import { cli, Strategy } from '../../registry.js';
import type { IPage } from '../../types.js';
export const extractCodeCommand = cli({
site: 'cursor',
name: 'extract-code',
description: 'Extract multi-line code blocks from the current Cursor conversation',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [],
columns: ['Code'],
func: async (page: IPage) => {
const blocks = await page.evaluate(`
(function() {
// Find standard pre/code blocks
let elements = Array.from(document.querySelectorAll('pre code, .markdown-root pre'));
// Fallback to Monaco editor content inside the UI
if (elements.length === 0) {
elements = Array.from(document.querySelectorAll('.monaco-editor'));
}
// Generic fallback to any code tag that spans multiple lines
if (elements.length === 0) {
elements = Array.from(document.querySelectorAll('code')).filter(c => c.innerText.includes('\\n'));
}
return elements.map(el => el.innerText || el.textContent || '').filter(text => text.trim().length > 0);
})()
`);
if (!blocks || blocks.length === 0) {
return [{ Code: 'No code blocks found in Cursor.' }];
}
return blocks.map((code: string) => ({ Code: code }));
},
});
+57
View File
@@ -0,0 +1,57 @@
import { cli, Strategy } from '../../registry.js';
import type { IPage } from '../../types.js';
export const modelCommand = cli({
site: 'cursor',
name: 'model',
description: 'Get or switch the currently active AI model in Cursor',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [
{ name: 'model_name', required: false, positional: true, help: 'The ID of the model to switch to (e.g. claude-3.5-sonnet)' }
],
columns: ['Status', 'Model'],
func: async (page: IPage, kwargs: any) => {
const desiredModel = kwargs.model_name as string | undefined;
if (!desiredModel) {
// Just read the current model
const currentModel = await page.evaluate(`
(function() {
const m = document.querySelector('.composer-unified-dropdown-model span, [class*="unifiedmodeldropdown"] span');
return m ? m.textContent.trim() : 'Unknown or Not Found';
})()
`);
return [
{
Status: 'Active',
Model: currentModel,
},
];
} else {
// Try to switch model (click dropdown, type/select model)
const success = await page.evaluate(`
(function(targetModel) {
const dropdown = document.querySelector('.composer-unified-dropdown-model, [class*="unifiedmodeldropdown"]');
if (!dropdown) return 'Dropdown not found';
dropdown.click();
// After clicking, the DOM usually spawns a popup list.
// Because it's hard to predict exactly how the list renders,
// a simple scriptable approach is just to click it, and hope we can select it via UI.
// In many React apps, clicking it opens a menu, and clicking the item works.
return 'Dropdown opened. Automated switching is not fully generic. Please implement precise list navigation depending on DOM.';
})(${JSON.stringify(desiredModel)})
`);
return [
{
Status: success,
Model: desiredModel,
},
];
}
},
});
+32
View File
@@ -0,0 +1,32 @@
import { cli, Strategy } from '../../registry.js';
import type { IPage } from '../../types.js';
export const newCommand = cli({
site: 'cursor',
name: 'new',
description: 'Start a new Cursor chat or Composer session',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
columns: ['Status'],
func: async (page: IPage) => {
const success = await page.evaluate(`
(function() {
const newChatButton = document.querySelector('[aria-label="New Chat"], [aria-label="New Chat (⌘N)"], .agent-sidebar-new-agent-button');
if (newChatButton) {
newChatButton.click();
return true;
}
return false;
})()
`);
if (!success) {
throw new Error('Could not find New Chat button in Cursor DOM.');
}
await page.wait(1);
return [{ Status: 'Success' }];
},
});
+47
View File
@@ -0,0 +1,47 @@
import { cli, Strategy } from '../../registry.js';
import type { IPage } from '../../types.js';
export const readCommand = cli({
site: 'cursor',
name: 'read',
description: 'Read the current Cursor chat/composer conversation history',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
columns: ['Role', 'Text'],
func: async (page: IPage) => {
const history = await page.evaluate(`
(function() {
const messages = Array.from(document.querySelectorAll('[data-message-role]'));
if (messages.length === 0) {
return [];
}
return messages.map(msg => {
const role = msg.getAttribute('data-message-role');
let text = '';
// Try to get structured markdown root for AI, or lexical text for human
const markdownRoot = msg.querySelector('.markdown-root');
if (markdownRoot) {
text = markdownRoot.innerText || markdownRoot.textContent;
} else {
text = msg.innerText || msg.textContent;
}
return {
Role: role === 'human' ? 'User' : 'Assistant',
Text: text.trim()
};
});
})()
`);
if (!history || history.length === 0) {
throw new Error('No conversation history found in Cursor.');
}
return history;
},
});
+47
View File
@@ -0,0 +1,47 @@
import { cli, Strategy } from '../../registry.js';
import type { IPage } from '../../types.js';
export const sendCommand = cli({
site: 'cursor',
name: 'send',
description: 'Send a prompt directly into Cursor Composer/Chat',
domain: 'localhost',
strategy: Strategy.UI,
browser: true,
args: [{ name: 'text', required: true, positional: true, help: 'Text to send into Cursor' }],
columns: ['Status', 'InjectedText'],
func: async (page: IPage, kwargs: any) => {
const textToInsert = kwargs.text as string;
const injected = await page.evaluate(
`(function(text) {
// Find the Lexical editor input for Composer or Chat
let composer = document.querySelector('.aislash-editor-input, [data-lexical-editor="true"], [contenteditable="true"]');
if (!composer) {
return false;
}
composer.focus();
document.execCommand('insertText', false, text);
return true;
})(${JSON.stringify(textToInsert)})`
);
if (!injected) {
throw new Error('Could not find Cursor Composer input element.');
}
// Submit the command. In Cursor, Enter usually submits the chat.
await page.wait(0.5);
await page.pressKey('Enter');
await page.wait(1);
return [
{
Status: 'Success',
InjectedText: textToInsert,
},
];
},
});
+23
View File
@@ -0,0 +1,23 @@
import { cli, Strategy } from '../../registry.js';
export const statusCommand = cli({
site: 'cursor',
name: 'status',
description: 'Check active CDP connection to Cursor AI Editor',
domain: 'localhost',
strategy: Strategy.UI, // Interactive UI manipulation
browser: true,
columns: ['Status', 'Url', 'Title'],
func: async (page) => {
const url = await page.evaluate('window.location.href');
const title = await page.evaluate('document.title');
return [
{
Status: 'Connected',
Url: url,
Title: title,
},
];
},
});