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:
jackwener committed 2026-03-19 23:51:36 +08:00
1 parent 89947fee50
commit fde618063f
8 files changed
+17 -192

No files matched your search

-3
View File
@@ -10,9 +10,6 @@
"activeTab",
"alarms"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "dist/background.js",
"type": "module"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "opencli-extension",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"type": "module",
"scripts": {
-150
View File
@@ -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
View File
@@ -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');
+1 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();