chore: pre-release cleanup
- Delete unused extension/src/executor.ts (chrome.scripting experiment) - Remove 15 no-op backward-compat exports from doctor.ts - Remove getTokenFingerprint no-op from browser/index.ts - Rename PlaywrightMCP → BrowserBridge across all source files (backward-compat alias kept in mcp.ts and browser/index.ts) - Remove unnecessary host_permissions from extension manifest - Sync extension package.json version to 0.2.0 - All 14 tests pass
This commit is contained in:
1 parent
89947fee50
commit
fde618063f
8 files changed
+17
-192
No files matched your search
@@ -10,9 +10,6 @@
|
||||
"activeTab",
|
||||
"alarms"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "dist/background.js",
|
||||
"type": "module"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "opencli-extension",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
/**
|
||||
* Stealth JS executor via chrome.scripting API.
|
||||
*
|
||||
* Architecture:
|
||||
* 1. Inject code as a <script> tag in MAIN world (bypasses CSP where eval is blocked)
|
||||
* 2. Script stores result in a hidden DOM element
|
||||
* 3. ISOLATED world script polls and reads the result
|
||||
*
|
||||
* This avoids both:
|
||||
* - CSP `unsafe-eval` restrictions (no eval() used)
|
||||
* - chrome.scripting MAIN world async return value limitations
|
||||
*/
|
||||
|
||||
// Monotonic counter for unique result IDs
|
||||
let _evalCounter = 0;
|
||||
|
||||
/**
|
||||
* Normalize JS code for evaluation:
|
||||
* - Already an IIFE → as-is
|
||||
* - Arrow/function literal → wrap as IIFE
|
||||
* - Everything else → bare expression
|
||||
*/
|
||||
function wrapForEval(js: string): string {
|
||||
const code = js.trim();
|
||||
if (!code) return 'undefined';
|
||||
if (/^\([\s\S]*\)\s*\(.*\)\s*$/.test(code)) return code;
|
||||
if (/^(async\s+)?(\([^)]*\)|[A-Za-z_]\w*)\s*=>/.test(code)) return `(${code})()`;
|
||||
if (/^(async\s+)?function[\s(]/.test(code)) return `(${code})()`;
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate JS in the target tab's MAIN world via script tag injection.
|
||||
* Safe against CSP restrictions that block eval().
|
||||
*/
|
||||
export async function evaluate(tabId: number, expression: string): Promise<unknown> {
|
||||
const code = wrapForEval(expression.trim());
|
||||
const resultId = `__opencli_r${++_evalCounter}_${Date.now()}`;
|
||||
|
||||
// Step 1: Create result holder + inject script tag in MAIN world
|
||||
try {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
world: 'MAIN',
|
||||
func: (userCode: string, rid: string) => {
|
||||
// Create hidden element to store result
|
||||
const holder = document.createElement('div');
|
||||
holder.id = rid;
|
||||
holder.style.display = 'none';
|
||||
document.documentElement.appendChild(holder);
|
||||
|
||||
// Create and inject a script tag with the user code wrapped in async handler
|
||||
const script = document.createElement('script');
|
||||
script.textContent = `
|
||||
(async () => {
|
||||
const __holder = document.getElementById(${JSON.stringify(rid)});
|
||||
try {
|
||||
const __result = await (async () => { return ${userCode}; })();
|
||||
__holder.dataset.result = JSON.stringify({ ok: true, data: __result });
|
||||
} catch (__err) {
|
||||
__holder.dataset.result = JSON.stringify({ ok: false, error: __err?.message || String(__err) });
|
||||
}
|
||||
})();
|
||||
`;
|
||||
document.documentElement.appendChild(script);
|
||||
// Clean up the script tag immediately (it's already executed)
|
||||
script.remove();
|
||||
},
|
||||
args: [code, resultId],
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`executeScript failed: ${msg}`);
|
||||
}
|
||||
|
||||
// Step 2: Poll from ISOLATED world to read the result
|
||||
const maxWait = 120000;
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start < maxWait) {
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
// ISOLATED world can still read DOM elements & dataset
|
||||
func: (rid: string) => {
|
||||
const el = document.getElementById(rid);
|
||||
if (!el) return null;
|
||||
const result = el.dataset.result;
|
||||
if (!result) return null;
|
||||
el.remove();
|
||||
return result;
|
||||
},
|
||||
args: [resultId],
|
||||
});
|
||||
|
||||
const resultText = results?.[0]?.result as string | null;
|
||||
if (resultText) {
|
||||
try {
|
||||
const parsed = JSON.parse(resultText);
|
||||
if (!parsed.ok) throw new Error(parsed.error);
|
||||
return parsed.data;
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) return resultText;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
|
||||
// Cleanup on timeout
|
||||
try {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
func: (rid: string) => document.getElementById(rid)?.remove(),
|
||||
args: [resultId],
|
||||
});
|
||||
} catch { /* ignore */ }
|
||||
|
||||
throw new Error('Evaluate timeout (120s)');
|
||||
}
|
||||
|
||||
export const evaluateAsync = evaluate;
|
||||
|
||||
/**
|
||||
* Capture a screenshot via chrome.tabs.captureVisibleTab().
|
||||
*/
|
||||
export async function screenshot(
|
||||
tabId: number,
|
||||
options: { format?: 'png' | 'jpeg'; quality?: number; fullPage?: boolean } = {},
|
||||
): Promise<string> {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
if (!tab.active) {
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
|
||||
const format = options.format ?? 'png';
|
||||
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId!, {
|
||||
format,
|
||||
quality: format === 'jpeg' ? (options.quality ?? 80) : undefined,
|
||||
});
|
||||
|
||||
return dataUrl.replace(/^data:image\/\w+;base64,/, '');
|
||||
}
|
||||
|
||||
/** No-op in scripting mode */
|
||||
export function detach(_tabId: number): void {}
|
||||
|
||||
/** No-op in scripting mode */
|
||||
export function registerListeners(): void {}
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, it, expect, vi } from 'vitest';
|
||||
import { PlaywrightMCP, __test__ } from './browser/index.js';
|
||||
import { BrowserBridge, __test__ } from './browser/index.js';
|
||||
|
||||
describe('browser helpers', () => {
|
||||
it('extracts tab entries from string snapshots', () => {
|
||||
@@ -45,9 +45,9 @@ describe('browser helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('PlaywrightMCP state', () => {
|
||||
describe('BrowserBridge state', () => {
|
||||
it('transitions to closed after close()', async () => {
|
||||
const mcp = new PlaywrightMCP();
|
||||
const mcp = new BrowserBridge();
|
||||
|
||||
expect(mcp.state).toBe('idle');
|
||||
|
||||
@@ -57,21 +57,21 @@ describe('PlaywrightMCP state', () => {
|
||||
});
|
||||
|
||||
it('rejects connect() after the session has been closed', async () => {
|
||||
const mcp = new PlaywrightMCP();
|
||||
const mcp = new BrowserBridge();
|
||||
await mcp.close();
|
||||
|
||||
await expect(mcp.connect()).rejects.toThrow('Session is closed');
|
||||
});
|
||||
|
||||
it('rejects connect() while already connecting', async () => {
|
||||
const mcp = new PlaywrightMCP();
|
||||
const mcp = new BrowserBridge();
|
||||
(mcp as any)._state = 'connecting';
|
||||
|
||||
await expect(mcp.connect()).rejects.toThrow('Already connecting');
|
||||
});
|
||||
|
||||
it('rejects connect() while closing', async () => {
|
||||
const mcp = new PlaywrightMCP();
|
||||
const mcp = new BrowserBridge();
|
||||
(mcp as any)._state = 'closing';
|
||||
|
||||
await expect(mcp.connect()).rejects.toThrow('Session is closing');
|
||||
|
||||
@@ -6,14 +6,9 @@
|
||||
*/
|
||||
|
||||
export { Page } from './page.js';
|
||||
export { PlaywrightMCP } from './mcp.js';
|
||||
export { BrowserBridge, BrowserBridge as PlaywrightMCP } from './mcp.js';
|
||||
export { isDaemonRunning } from './daemon-client.js';
|
||||
|
||||
// Backward compatibility: getTokenFingerprint is no longer needed but kept as no-op export
|
||||
export function getTokenFingerprint(_token: string | undefined): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
||||
import { withTimeoutMs } from '../runtime.js';
|
||||
|
||||
|
||||
+2
-19
@@ -7,7 +7,7 @@
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { checkDaemonStatus } from './browser/discover.js';
|
||||
import { PlaywrightMCP } from './browser/index.js';
|
||||
import { BrowserBridge } from './browser/index.js';
|
||||
import { browserSession } from './runtime.js';
|
||||
|
||||
export type DoctorOptions = {
|
||||
@@ -37,7 +37,7 @@ export type DoctorReport = {
|
||||
export async function checkConnectivity(opts?: { timeout?: number }): Promise<ConnectivityResult> {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const mcp = new PlaywrightMCP();
|
||||
const mcp = new BrowserBridge();
|
||||
const page = await mcp.connect({ timeout: opts?.timeout ?? 8 });
|
||||
// Try a simple eval to verify end-to-end connectivity
|
||||
await page.evaluate('1 + 1');
|
||||
@@ -116,20 +116,3 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Backward compatibility exports (no-ops for things that no longer exist)
|
||||
export const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
|
||||
export function discoverExtensionToken(): string | null { return null; }
|
||||
export function checkExtensionInstalled(): { installed: boolean; browsers: string[] } { return { installed: false, browsers: [] }; }
|
||||
export function applyBrowserDoctorFix(): Promise<string[]> { return Promise.resolve([]); }
|
||||
export function getDefaultShellRcPath(): string { return ''; }
|
||||
export function getDefaultMcpConfigPaths(): string[] { return []; }
|
||||
export function readTokenFromShellContent(_content: string): string | null { return null; }
|
||||
export function upsertShellToken(content: string): string { return content; }
|
||||
export function upsertJsonConfigToken(content: string): string { return content; }
|
||||
export function readTomlConfigToken(_content: string): string | null { return null; }
|
||||
export function upsertTomlConfigToken(content: string): string { return content; }
|
||||
export function shortenPath(p: string): string { return p; }
|
||||
export function toolName(_p: string): string { return ''; }
|
||||
export function fileExists(filePath: string): boolean { try { return require('node:fs').existsSync(filePath); } catch { return false; } }
|
||||
export function writeFileWithMkdir(_p: string, _c: string): void {}
|
||||
export async function checkTokenConnectivity(opts?: { timeout?: number }): Promise<ConnectivityResult> { return checkConnectivity(opts); }
|
||||
+5
-5
@@ -11,7 +11,7 @@ import chalk from 'chalk';
|
||||
import { discoverClis, executeCommand } from './engine.js';
|
||||
import { Strategy, type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
|
||||
import { render as renderOutput } from './output.js';
|
||||
import { PlaywrightMCP } from './browser/index.js';
|
||||
import { BrowserBridge } from './browser/index.js';
|
||||
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
|
||||
import { PKG_VERSION } from './version.js';
|
||||
import { getCompletions, printCompletionScript } from './completion.js';
|
||||
@@ -99,18 +99,18 @@ program.command('verify').description('Validate + smoke test').argument('[target
|
||||
});
|
||||
|
||||
program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('<url>').option('--site <name>').option('--goal <text>').option('--wait <s>', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)').option('--click <labels>', 'Comma-separated labels to click before fuzzing (e.g. "字幕,CC,评论")')
|
||||
.action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: PlaywrightMCP, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
|
||||
.action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserBridge, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
|
||||
|
||||
program.command('synthesize').description('Synthesize CLIs from explore').argument('<target>').option('--top <n>', '', '3')
|
||||
.action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });
|
||||
|
||||
program.command('generate').description('One-shot: explore → synthesize → register').argument('<url>').option('--goal <text>').option('--site <name>')
|
||||
.action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const r = await generateCliFromUrl({ url, BrowserFactory: PlaywrightMCP, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
|
||||
.action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const r = await generateCliFromUrl({ url, BrowserFactory: BrowserBridge, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
|
||||
|
||||
program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('<url>').option('--site <name>')
|
||||
.action(async (url, opts) => {
|
||||
const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
|
||||
const result = await browserSession(PlaywrightMCP, async (page) => {
|
||||
const result = await browserSession(BrowserBridge, async (page) => {
|
||||
// Navigate to the site first for cookie context
|
||||
try { const siteUrl = new URL(url); await page.goto(`${siteUrl.protocol}//${siteUrl.host}`); await page.wait(2); } catch {}
|
||||
return cascadeProbe(page, url);
|
||||
@@ -192,7 +192,7 @@ for (const [, cmd] of registry) {
|
||||
if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
|
||||
let result: any;
|
||||
if (cmd.browser) {
|
||||
result = await browserSession(PlaywrightMCP, async (page) => {
|
||||
result = await browserSession(BrowserBridge, async (page) => {
|
||||
// Cookie/header strategies require same-origin context for credentialed fetch.
|
||||
// In CDP mode the active tab may be on an unrelated domain, causing CORS failures.
|
||||
// Navigate to the command's domain first (mirrors cascade command behavior).
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@
|
||||
import chalk from 'chalk';
|
||||
import { checkDaemonStatus } from './browser/discover.js';
|
||||
import { checkConnectivity } from './doctor.js';
|
||||
import { PlaywrightMCP } from './browser/index.js';
|
||||
import { BrowserBridge } from './browser/index.js';
|
||||
|
||||
export async function runSetup(opts: { cliVersion?: string; token?: string } = {}) {
|
||||
console.log();
|
||||
@@ -27,7 +27,7 @@ export async function runSetup(opts: { cliVersion?: string; token?: string } = {
|
||||
console.log(chalk.dim(' Starting daemon now...'));
|
||||
|
||||
// Try to spawn daemon
|
||||
const mcp = new PlaywrightMCP();
|
||||
const mcp = new BrowserBridge();
|
||||
try {
|
||||
await mcp.connect({ timeout: 5 });
|
||||
await mcp.close();
|
||||
|
||||
Reference in new issue
Block a user