Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17cc44fa21 | |||
| ff72cbf121 | |||
| 3e77889e67 |
@@ -28,6 +28,8 @@ total=0
|
||||
|
||||
for adapter_dir in "$SRC_DIR"/*/; do
|
||||
adapter_name="$(basename "$adapter_dir")"
|
||||
# Skip internal directories (e.g., _shared)
|
||||
[[ "$adapter_name" == _* ]] && continue
|
||||
total=$((total + 1))
|
||||
|
||||
# Check if doc exists in browser/ or desktop/ subdirectories
|
||||
|
||||
+2
-2
@@ -14,11 +14,11 @@ describe('browser helpers', () => {
|
||||
|
||||
it('extracts tab entries from MCP markdown format', () => {
|
||||
const entries = __test__.extractTabEntries(
|
||||
'- 0: (current) [Playwright MCP extension](chrome-extension://abc/connect.html)\n- 1: [知乎 - 首页](https://www.zhihu.com/)'
|
||||
'- 0: (current) [Browser Bridge extension](chrome-extension://abc/connect.html)\n- 1: [知乎 - 首页](https://www.zhihu.com/)'
|
||||
);
|
||||
|
||||
expect(entries).toEqual([
|
||||
{ index: 0, identity: '(current) [Playwright MCP extension](chrome-extension://abc/connect.html)' },
|
||||
{ index: 0, identity: '(current) [Browser Bridge extension](chrome-extension://abc/connect.html)' },
|
||||
{ index: 1, identity: '[知乎 - 首页](https://www.zhihu.com/)' },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
/**
|
||||
* Daemon discovery — simplified from MCP server path discovery.
|
||||
*
|
||||
* Only needs to check if the daemon is running. No more file system
|
||||
* scanning for @playwright/mcp locations.
|
||||
* Daemon discovery — checks if the daemon is running.
|
||||
*/
|
||||
|
||||
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
export { Page } from './page.js';
|
||||
export { BrowserBridge, BrowserBridge as PlaywrightMCP } from './mcp.js';
|
||||
export { BrowserBridge } from './mcp.js';
|
||||
export { CDPBridge } from './cdp.js';
|
||||
export { isDaemonRunning } from './daemon-client.js';
|
||||
export { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
||||
|
||||
@@ -117,6 +117,3 @@ export class BrowserBridge {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use BrowserBridge instead */
|
||||
export const PlaywrightMCP = BrowserBridge;
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ export function extractTabEntries(raw: unknown): Array<{ index: number; identity
|
||||
.map(line => line.trim())
|
||||
.filter(Boolean)
|
||||
.map(line => {
|
||||
// Match actual Playwright MCP format: "- 0: (current) [title](url)" or "- 1: [title](url)"
|
||||
// Match tab list format: "- 0: (current) [title](url)" or "- 1: [title](url)"
|
||||
const mcpMatch = line.match(/^-\s+(\d+):\s*(.*)$/);
|
||||
if (mcpMatch) {
|
||||
return {
|
||||
|
||||
+1
-23
@@ -46,29 +46,7 @@ export interface ManifestEntry {
|
||||
navigateBefore?: boolean | string;
|
||||
}
|
||||
|
||||
interface YamlArgDefinition {
|
||||
type?: string;
|
||||
default?: unknown;
|
||||
required?: boolean;
|
||||
positional?: boolean;
|
||||
description?: string;
|
||||
help?: string;
|
||||
choices?: string[];
|
||||
}
|
||||
|
||||
interface YamlCliDefinition {
|
||||
site?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
domain?: string;
|
||||
strategy?: string;
|
||||
browser?: boolean;
|
||||
args?: Record<string, YamlArgDefinition>;
|
||||
columns?: string[];
|
||||
pipeline?: Record<string, unknown>[];
|
||||
timeout?: number;
|
||||
navigateBefore?: boolean | string;
|
||||
}
|
||||
import type { YamlCliDefinition } from './yaml-schema.js';
|
||||
|
||||
import { isRecord } from './utils.js';
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Shared command factories for Electron/desktop app adapters.
|
||||
* Eliminates duplicate screenshot/status/new/dump implementations
|
||||
* across cursor, codex, chatwise, etc.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
/**
|
||||
* Factory: capture DOM HTML + accessibility snapshot.
|
||||
*/
|
||||
export function makeScreenshotCommand(site: string, displayName?: string) {
|
||||
const label = displayName ?? site;
|
||||
return cli({
|
||||
site,
|
||||
name: 'screenshot',
|
||||
description: `Capture a snapshot of the current ${label} window (DOM + Accessibility tree)`,
|
||||
domain: 'localhost',
|
||||
strategy: Strategy.UI,
|
||||
browser: true,
|
||||
args: [
|
||||
{ name: 'output', required: false, help: `Output file path (default: /tmp/${site}-snapshot.txt)` },
|
||||
],
|
||||
columns: ['Status', 'File'],
|
||||
func: async (page: IPage, kwargs: any) => {
|
||||
const outputPath = (kwargs.output as string) || `/tmp/${site}-snapshot.txt`;
|
||||
|
||||
const snap = await page.snapshot({ compact: true });
|
||||
const html = await page.evaluate('document.documentElement.outerHTML');
|
||||
|
||||
const htmlPath = outputPath.replace(/\.\w+$/, '') + '-dom.html';
|
||||
const snapPath = outputPath.replace(/\.\w+$/, '') + '-a11y.txt';
|
||||
|
||||
fs.writeFileSync(htmlPath, html);
|
||||
fs.writeFileSync(snapPath, typeof snap === 'string' ? snap : JSON.stringify(snap, null, 2));
|
||||
|
||||
return [
|
||||
{ Status: 'Success', File: htmlPath },
|
||||
{ Status: 'Success', File: snapPath },
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: check CDP connection status.
|
||||
*/
|
||||
export function makeStatusCommand(site: string, displayName?: string) {
|
||||
const label = displayName ?? site;
|
||||
return cli({
|
||||
site,
|
||||
name: 'status',
|
||||
description: `Check active CDP connection to ${label}`,
|
||||
domain: 'localhost',
|
||||
strategy: Strategy.UI,
|
||||
browser: true,
|
||||
columns: ['Status', 'Url', 'Title'],
|
||||
func: async (page: IPage) => {
|
||||
const url = await page.evaluate('window.location.href');
|
||||
const title = await page.evaluate('document.title');
|
||||
return [{ Status: 'Connected', Url: url, Title: title }];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: start a new session via Cmd/Ctrl+N.
|
||||
*/
|
||||
export function makeNewCommand(site: string, displayName?: string) {
|
||||
const label = displayName ?? site;
|
||||
return cli({
|
||||
site,
|
||||
name: 'new',
|
||||
description: `Start a new ${label} session`,
|
||||
domain: 'localhost',
|
||||
strategy: Strategy.UI,
|
||||
browser: true,
|
||||
columns: ['Status'],
|
||||
func: async (page: IPage) => {
|
||||
const isMac = process.platform === 'darwin';
|
||||
await page.pressKey(isMac ? 'Meta+N' : 'Control+N');
|
||||
await page.wait(1);
|
||||
return [{ Status: 'Success' }];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: dump DOM + snapshot for reverse-engineering.
|
||||
*/
|
||||
export function makeDumpCommand(site: string) {
|
||||
return cli({
|
||||
site,
|
||||
name: 'dump',
|
||||
description: `Dump the DOM and Accessibility tree of ${site} for reverse-engineering`,
|
||||
domain: 'localhost',
|
||||
strategy: Strategy.UI,
|
||||
browser: true,
|
||||
columns: ['action', 'files'],
|
||||
func: async (page: IPage) => {
|
||||
const dom = await page.evaluate('document.body.innerHTML');
|
||||
fs.writeFileSync(`/tmp/${site}-dom.html`, dom);
|
||||
|
||||
const snap = await page.snapshot({ interactive: false });
|
||||
fs.writeFileSync(`/tmp/${site}-snapshot.json`, JSON.stringify(snap, null, 2));
|
||||
|
||||
return [
|
||||
{
|
||||
action: 'Dom extraction finished',
|
||||
files: `/tmp/${site}-dom.html, /tmp/${site}-snapshot.json`,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,21 +1,3 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
import { makeNewCommand } from '../_shared/desktop-commands.js';
|
||||
|
||||
export const newCommand = cli({
|
||||
site: 'chatwise',
|
||||
name: 'new',
|
||||
description: 'Start a new conversation in ChatWise',
|
||||
domain: 'localhost',
|
||||
strategy: Strategy.UI,
|
||||
browser: true,
|
||||
args: [],
|
||||
columns: ['Status'],
|
||||
func: async (page: IPage) => {
|
||||
// ChatWise uses standard Electron shortcuts
|
||||
const isMac = process.platform === 'darwin';
|
||||
await page.pressKey(isMac ? 'Meta+N' : 'Control+N');
|
||||
await page.wait(1);
|
||||
|
||||
return [{ Status: 'Success' }];
|
||||
},
|
||||
});
|
||||
export const newCommand = makeNewCommand('chatwise', 'ChatWise conversation');
|
||||
|
||||
@@ -1,33 +1,3 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
import { makeScreenshotCommand } from '../_shared/desktop-commands.js';
|
||||
|
||||
export const screenshotCommand = cli({
|
||||
site: 'chatwise',
|
||||
name: 'screenshot',
|
||||
description: 'Capture a snapshot of the current ChatWise window (DOM + Accessibility tree)',
|
||||
domain: 'localhost',
|
||||
strategy: Strategy.UI,
|
||||
browser: true,
|
||||
args: [
|
||||
{ name: 'output', required: false, help: 'Output file path (default: /tmp/chatwise-snapshot)' },
|
||||
],
|
||||
columns: ['Status', 'File'],
|
||||
func: async (page: IPage, kwargs: any) => {
|
||||
const basePath = (kwargs.output as string) || '/tmp/chatwise-snapshot';
|
||||
|
||||
const snap = await page.snapshot({ compact: true });
|
||||
const html = await page.evaluate('document.documentElement.outerHTML');
|
||||
|
||||
const htmlPath = basePath + '-dom.html';
|
||||
const snapPath = basePath + '-a11y.txt';
|
||||
|
||||
fs.writeFileSync(htmlPath, html);
|
||||
fs.writeFileSync(snapPath, typeof snap === 'string' ? snap : JSON.stringify(snap, null, 2));
|
||||
|
||||
return [
|
||||
{ Status: 'Success', File: htmlPath },
|
||||
{ Status: 'Success', File: snapPath },
|
||||
];
|
||||
},
|
||||
});
|
||||
export const screenshotCommand = makeScreenshotCommand('chatwise', 'ChatWise');
|
||||
|
||||
@@ -1,25 +1,3 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
import { makeStatusCommand } from '../_shared/desktop-commands.js';
|
||||
|
||||
export const statusCommand = cli({
|
||||
site: 'chatwise',
|
||||
name: 'status',
|
||||
description: 'Check active CDP connection to ChatWise Desktop',
|
||||
domain: 'localhost',
|
||||
strategy: Strategy.UI,
|
||||
browser: true,
|
||||
args: [],
|
||||
columns: ['Status', 'Url', 'Title'],
|
||||
func: async (page: IPage) => {
|
||||
const url = await page.evaluate('window.location.href');
|
||||
const title = await page.evaluate('document.title');
|
||||
|
||||
return [
|
||||
{
|
||||
Status: 'Connected',
|
||||
Url: url,
|
||||
Title: title,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
export const statusCommand = makeStatusCommand('chatwise', 'ChatWise Desktop');
|
||||
|
||||
+2
-27
@@ -1,28 +1,3 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import * as fs from 'fs';
|
||||
import { makeDumpCommand } from '../_shared/desktop-commands.js';
|
||||
|
||||
export const dumpCommand = cli({
|
||||
site: 'codex',
|
||||
name: 'dump',
|
||||
description: 'Dump the DOM and Accessibility tree of Codex 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/codex-dom.html', dom);
|
||||
|
||||
// Get accessibility snapshot
|
||||
const snap = await page.snapshot({ interactive: false });
|
||||
fs.writeFileSync('/tmp/codex-snapshot.json', JSON.stringify(snap, null, 2));
|
||||
|
||||
return [
|
||||
{
|
||||
action: 'Dom extraction finished',
|
||||
files: '/tmp/codex-dom.html, /tmp/codex-snapshot.json',
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
export const dumpCommand = makeDumpCommand('codex');
|
||||
|
||||
@@ -23,7 +23,7 @@ export const modelCommand = cli({
|
||||
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[].
|
||||
// Not directly in main DOM, might be in a webview — evaluate doesn't cross origin boundaries 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';
|
||||
|
||||
+2
-28
@@ -1,29 +1,3 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import { makeNewCommand } from '../_shared/desktop-commands.js';
|
||||
|
||||
export const newCommand = cli({
|
||||
site: 'codex',
|
||||
name: 'new',
|
||||
description: 'Start a new Codex conversation thread / isolated workspace',
|
||||
domain: 'localhost',
|
||||
strategy: Strategy.UI,
|
||||
browser: true,
|
||||
columns: ['Status', 'Action'],
|
||||
func: async (page) => {
|
||||
// According to research, Cmd+N / Ctrl+N spins up a new thread
|
||||
const isMac = process.platform === 'darwin';
|
||||
const newThreadKey = isMac ? 'Meta+N' : 'Control+N';
|
||||
|
||||
// Simulate keyboard shortcut
|
||||
await page.pressKey(newThreadKey);
|
||||
|
||||
// Wait a brief moment for UI animation
|
||||
await page.wait(1);
|
||||
|
||||
return [
|
||||
{
|
||||
Status: 'Success',
|
||||
Action: `Pressed ${newThreadKey} to trigger New Thread`,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
export const newCommand = makeNewCommand('codex', 'Codex conversation');
|
||||
|
||||
@@ -1,33 +1,3 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
import { makeScreenshotCommand } from '../_shared/desktop-commands.js';
|
||||
|
||||
export const screenshotCommand = cli({
|
||||
site: 'codex',
|
||||
name: 'screenshot',
|
||||
description: 'Capture a snapshot of the current Codex window (DOM + Accessibility tree)',
|
||||
domain: 'localhost',
|
||||
strategy: Strategy.UI,
|
||||
browser: true,
|
||||
args: [
|
||||
{ name: 'output', required: false, help: 'Output file path (default: /tmp/codex-snapshot.txt)' },
|
||||
],
|
||||
columns: ['Status', 'File'],
|
||||
func: async (page: IPage, kwargs: any) => {
|
||||
const outputPath = (kwargs.output as string) || '/tmp/codex-snapshot.txt';
|
||||
|
||||
const snap = await page.snapshot({ compact: true });
|
||||
const html = await page.evaluate('document.documentElement.outerHTML');
|
||||
|
||||
const htmlPath = outputPath.replace(/\.\w+$/, '') + '-dom.html';
|
||||
const snapPath = outputPath.replace(/\.\w+$/, '') + '-a11y.txt';
|
||||
|
||||
fs.writeFileSync(htmlPath, html);
|
||||
fs.writeFileSync(snapPath, typeof snap === 'string' ? snap : JSON.stringify(snap, null, 2));
|
||||
|
||||
return [
|
||||
{ Status: 'Success', File: htmlPath },
|
||||
{ Status: 'Success', File: snapPath },
|
||||
];
|
||||
},
|
||||
});
|
||||
export const screenshotCommand = makeScreenshotCommand('codex', 'Codex');
|
||||
|
||||
@@ -1,25 +1,3 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
import { makeStatusCommand } from '../_shared/desktop-commands.js';
|
||||
|
||||
export const statusCommand = cli({
|
||||
site: 'codex',
|
||||
name: 'status',
|
||||
description: 'Check active CDP connection to OpenAI Codex App',
|
||||
domain: 'localhost',
|
||||
strategy: Strategy.UI,
|
||||
browser: true,
|
||||
args: [],
|
||||
columns: ['Status', 'Url', 'Title'],
|
||||
func: async (page: IPage) => {
|
||||
const url = await page.evaluate('window.location.href');
|
||||
const title = await page.evaluate('document.title');
|
||||
|
||||
return [
|
||||
{
|
||||
Status: 'Connected',
|
||||
Url: url,
|
||||
Title: title,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
export const statusCommand = makeStatusCommand('codex', 'OpenAI Codex App');
|
||||
|
||||
+2
-27
@@ -1,28 +1,3 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import * as fs from 'fs';
|
||||
import { makeDumpCommand } from '../_shared/desktop-commands.js';
|
||||
|
||||
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',
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
export const dumpCommand = makeDumpCommand('cursor');
|
||||
|
||||
+2
-20
@@ -1,21 +1,3 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
import { makeNewCommand } from '../_shared/desktop-commands.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,
|
||||
args: [],
|
||||
columns: ['Status'],
|
||||
func: async (page: IPage) => {
|
||||
// Use keyboard shortcut — most robust approach, avoids brittle DOM selectors
|
||||
const isMac = process.platform === 'darwin';
|
||||
await page.pressKey(isMac ? 'Meta+N' : 'Control+N');
|
||||
await page.wait(1);
|
||||
|
||||
return [{ Status: 'Success' }];
|
||||
},
|
||||
});
|
||||
export const newCommand = makeNewCommand('cursor', 'Cursor chat or Composer');
|
||||
|
||||
@@ -1,38 +1,3 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
function makeScreenshotCommand(site: string) {
|
||||
return cli({
|
||||
site,
|
||||
name: 'screenshot',
|
||||
description: `Capture a snapshot of the current ${site} window (DOM + Accessibility tree)`,
|
||||
domain: 'localhost',
|
||||
strategy: Strategy.UI,
|
||||
browser: true,
|
||||
args: [
|
||||
{ name: 'output', required: false, help: `Output file path (default: /tmp/${site}-snapshot.txt)` },
|
||||
],
|
||||
columns: ['Status', 'File'],
|
||||
func: async (page: IPage, kwargs: any) => {
|
||||
const outputPath = (kwargs.output as string) || `/tmp/${site}-snapshot.txt`;
|
||||
|
||||
// Get both the accessibility snapshot and the raw DOM HTML
|
||||
const snap = await page.snapshot({ compact: true });
|
||||
const html = await page.evaluate('document.documentElement.outerHTML');
|
||||
|
||||
const htmlPath = outputPath.replace(/\.\w+$/, '') + '-dom.html';
|
||||
const snapPath = outputPath.replace(/\.\w+$/, '') + '-a11y.txt';
|
||||
|
||||
fs.writeFileSync(htmlPath, html);
|
||||
fs.writeFileSync(snapPath, typeof snap === 'string' ? snap : JSON.stringify(snap, null, 2));
|
||||
|
||||
return [
|
||||
{ Status: 'Success', File: htmlPath },
|
||||
{ Status: 'Success', File: snapPath },
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
import { makeScreenshotCommand } from '../_shared/desktop-commands.js';
|
||||
|
||||
export const screenshotCursor = makeScreenshotCommand('cursor');
|
||||
|
||||
@@ -1,23 +1,3 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import { makeStatusCommand } from '../_shared/desktop-commands.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,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
export const statusCommand = makeStatusCommand('cursor', 'Cursor AI Editor');
|
||||
|
||||
+1
-24
@@ -22,29 +22,7 @@ import type { ManifestEntry } from './build-manifest.js';
|
||||
export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
|
||||
const CLI_MODULE_PATTERN = /\bcli\s*\(/;
|
||||
|
||||
interface YamlArgDefinition {
|
||||
type?: string;
|
||||
default?: unknown;
|
||||
required?: boolean;
|
||||
positional?: boolean;
|
||||
description?: string;
|
||||
help?: string;
|
||||
choices?: string[];
|
||||
}
|
||||
|
||||
interface YamlCliDefinition {
|
||||
site?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
domain?: string;
|
||||
strategy?: string;
|
||||
browser?: boolean;
|
||||
args?: Record<string, YamlArgDefinition>;
|
||||
columns?: string[];
|
||||
pipeline?: Record<string, unknown>[];
|
||||
timeout?: number;
|
||||
navigateBefore?: boolean | string;
|
||||
}
|
||||
import type { YamlCliDefinition } from './yaml-schema.js';
|
||||
|
||||
function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Strategy.COOKIE): Strategy {
|
||||
if (!rawStrategy) return fallback;
|
||||
@@ -136,7 +114,6 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
||||
*/
|
||||
async function discoverClisFromFs(dir: string): Promise<void> {
|
||||
try { await fs.promises.access(dir); } catch { return; }
|
||||
const promises: Promise<unknown>[] = [];
|
||||
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||
|
||||
const sitePromises = entries
|
||||
|
||||
+3
-15
@@ -11,6 +11,9 @@ import * as os from 'node:os';
|
||||
import { URL } from 'node:url';
|
||||
import type { ProgressBar } from './progress.js';
|
||||
import { isBinaryInstalled } from '../external.js';
|
||||
import type { BrowserCookie } from '../types.js';
|
||||
|
||||
export type { BrowserCookie } from '../types.js';
|
||||
|
||||
export interface DownloadOptions {
|
||||
cookies?: string;
|
||||
@@ -28,26 +31,11 @@ export interface YtdlpOptions {
|
||||
onProgress?: (percent: number) => void;
|
||||
}
|
||||
|
||||
export interface BrowserCookie {
|
||||
name: string;
|
||||
value: string;
|
||||
domain: string;
|
||||
path?: string;
|
||||
secure?: boolean;
|
||||
httpOnly?: boolean;
|
||||
expirationDate?: number;
|
||||
}
|
||||
|
||||
/** Check if yt-dlp is available in PATH. */
|
||||
export function checkYtdlp(): boolean {
|
||||
return isBinaryInstalled('yt-dlp');
|
||||
}
|
||||
|
||||
/** Check if ffmpeg is available in PATH. */
|
||||
export function checkFfmpeg(): boolean {
|
||||
return isBinaryInstalled('ffmpeg');
|
||||
}
|
||||
|
||||
/** Domains that host video content and can be downloaded via yt-dlp. */
|
||||
const VIDEO_PLATFORM_DOMAINS = [
|
||||
'youtube.com', 'youtu.be', 'bilibili.com', 'twitter.com',
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ export interface ExploreBundle {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse raw network output from Playwright MCP.
|
||||
* Parse raw network output from the browser bridge.
|
||||
* Handles text format: [GET] url => [200]
|
||||
*/
|
||||
function parseNetworkRequests(raw: unknown): NetworkEntry[] {
|
||||
|
||||
@@ -24,7 +24,6 @@ function parseEnvTimeout(envVar: string, fallback: number): number {
|
||||
export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_CONNECT_TIMEOUT', 30);
|
||||
export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_COMMAND_TIMEOUT', 60);
|
||||
export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_EXPLORE_TIMEOUT', 120);
|
||||
export const DEFAULT_BROWSER_SMOKE_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_SMOKE_TIMEOUT', 60);
|
||||
|
||||
/**
|
||||
* Timeout with seconds unit. Used for high-level command timeouts.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Tests for snapshotFormatter.ts: Playwright MCP snapshot tree filtering.
|
||||
* Tests for snapshotFormatter.ts: accessibility snapshot tree filtering.
|
||||
*
|
||||
* Uses sanitized excerpts from real websites (GitHub, Bilibili, Twitter)
|
||||
* to validate noise filtering, annotation stripping, and output quality.
|
||||
@@ -9,7 +9,7 @@ import { describe, it, expect } from 'vitest';
|
||||
import { formatSnapshot } from './snapshotFormatter.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fixtures: sanitized excerpts from real Playwright MCP snapshots
|
||||
// Fixtures: sanitized excerpts from real accessibility snapshots
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** GitHub dashboard navigation bar (generic-heavy, refs, /url: lines) */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Aria snapshot formatter: parses Playwright MCP snapshot text into clean format.
|
||||
* Aria snapshot formatter: parses accessibility snapshot text into clean format.
|
||||
*
|
||||
* Multi-pass pipeline:
|
||||
* 1. Parse & filter: strip annotations, metadata, noise roles, ads, decorators
|
||||
@@ -69,10 +69,10 @@ const BOILERPLATE_LABELS = [
|
||||
/**
|
||||
* Parse role and text from a trimmed snapshot line.
|
||||
* Handles quoted labels and trailing text after colon correctly,
|
||||
* including lines wrapped in single quotes by Playwright.
|
||||
* including lines wrapped in single quotes by the snapshot engine.
|
||||
*/
|
||||
function parseLine(trimmed: string): { role: string; text: string; hasText: boolean; trailingText: string } {
|
||||
// Unwrap outer single quotes if present (Playwright wraps lines with special chars)
|
||||
// Unwrap outer single quotes if present (snapshot engine wraps lines with special chars)
|
||||
let line = trimmed;
|
||||
if (line.startsWith("'") && line.endsWith("':")) {
|
||||
line = line.slice(1, -2) + ':';
|
||||
@@ -114,7 +114,7 @@ function parseLine(trimmed: string): { role: string; text: string; hasText: bool
|
||||
|
||||
/**
|
||||
* Strip ALL bracket annotations from a content line, preserving quoted strings.
|
||||
* Handles both double-quoted and outer single-quoted lines from Playwright.
|
||||
* Handles both double-quoted and outer single-quoted lines from the snapshot engine.
|
||||
*/
|
||||
function stripAnnotations(content: string): string {
|
||||
// Unwrap outer single quotes first
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Page interface: type-safe abstraction over Playwright MCP browser page.
|
||||
* Page interface: type-safe abstraction over the browser page.
|
||||
*
|
||||
* All pipeline steps and CLI adapters should use this interface
|
||||
* instead of `any` for browser interactions.
|
||||
|
||||
+3
-2
@@ -2,6 +2,9 @@
|
||||
* Shared utility functions used across the codebase.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
/** Type guard: checks if a value is a non-null, non-array object. */
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
@@ -30,8 +33,6 @@ export async function mapConcurrent<T, R>(
|
||||
|
||||
/** Save a base64-encoded string to a file, creating parent directories as needed. */
|
||||
export async function saveBase64ToFile(base64: string, filePath: string): Promise<void> {
|
||||
const fs = await import('node:fs');
|
||||
const path = await import('node:path');
|
||||
const dir = path.dirname(filePath);
|
||||
await fs.promises.mkdir(dir, { recursive: true });
|
||||
await fs.promises.writeFile(filePath, Buffer.from(base64, 'base64'));
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Shared YAML CLI definition types.
|
||||
* Used by both discovery.ts (runtime) and build-manifest.ts (build-time).
|
||||
*/
|
||||
|
||||
export interface YamlArgDefinition {
|
||||
type?: string;
|
||||
default?: unknown;
|
||||
required?: boolean;
|
||||
positional?: boolean;
|
||||
description?: string;
|
||||
help?: string;
|
||||
choices?: string[];
|
||||
}
|
||||
|
||||
export interface YamlCliDefinition {
|
||||
site?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
domain?: string;
|
||||
strategy?: string;
|
||||
browser?: boolean;
|
||||
args?: Record<string, YamlArgDefinition>;
|
||||
columns?: string[];
|
||||
pipeline?: Record<string, unknown>[];
|
||||
timeout?: number;
|
||||
navigateBefore?: boolean | string;
|
||||
}
|
||||
Reference in New Issue
Block a user