Compare commits

...

3 Commits

Author SHA1 Message Date
jackwener 17cc44fa21 chore: remove stale Playwright references from comments and test fixtures
Replace "Playwright MCP" with accurate terminology (browser bridge,
snapshot engine, etc.) in comments across the codebase. Functional
stealth code that detects/cleans __playwright globals is preserved.
2026-03-24 20:22:24 +08:00
jackwener ff72cbf121 fix(ci): skip _shared internal directory in doc coverage check 2026-03-24 20:20:03 +08:00
jackwener 3e77889e67 refactor: simplify codebase by removing dead code, deduplicating types, and extracting shared desktop adapter commands
- Remove dead code: unused `promises` array in discovery.ts, unused `DEFAULT_BROWSER_SMOKE_TIMEOUT`, unused `checkFfmpeg()`, deprecated `PlaywrightMCP` alias
- Extract shared `YamlArgDefinition`/`YamlCliDefinition` into `yaml-schema.ts` (was duplicated in discovery.ts and build-manifest.ts)
- Unify `BrowserCookie` type: remove duplicate from download/index.ts, re-export from types.ts
- Create `_shared/desktop-commands.ts` with factory functions (makeScreenshotCommand, makeStatusCommand, makeNewCommand, makeDumpCommand), simplifying 11 adapter files from ~20-30 lines each to 3 lines
- Fix unnecessary dynamic imports in utils.ts

Net: +30 / -361 lines
2026-03-24 20:17:31 +08:00
29 changed files with 190 additions and 377 deletions
+2
View File
@@ -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
View File
@@ -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 -4
View File
@@ -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';
+1 -1
View File
@@ -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';
-3
View File
@@ -117,6 +117,3 @@ export class BrowserBridge {
);
}
}
/** @deprecated Use BrowserBridge instead */
export const PlaywrightMCP = BrowserBridge;
+1 -1
View File
@@ -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
View File
@@ -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';
+117
View File
@@ -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`,
},
];
},
});
}
+2 -20
View File
@@ -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');
+2 -32
View File
@@ -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');
+2 -24
View File
@@ -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
View File
@@ -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');
+1 -1
View File
@@ -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
View File
@@ -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');
+2 -32
View File
@@ -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');
+2 -24
View File
@@ -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
View File
@@ -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
View File
@@ -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 -36
View File
@@ -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');
+2 -22
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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[] {
-1
View File
@@ -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.
+2 -2
View File
@@ -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) */
+4 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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'));
+28
View File
@@ -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;
}