feat: add advanced cursor and codex capabilities
This commit is contained in:
1 parent
981cc1bc5e
commit
b32fe1cbc3
13 files changed
+418
-3
No files matched your search
@@ -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 |
|
||||
|
||||
@@ -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 | 🔐 浏览器 |
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@jackwener/opencli",
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.3",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
@@ -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 }));
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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' }];
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user