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
@@ -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