From a1178cbd35630a7e2bd79c2d50722ce0386ee902 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Wed, 4 Feb 2026 13:09:41 +0100 Subject: [PATCH] add Ghost Browser API integration Exposes chrome.ghostPublicAPI, chrome.ghostProxies, and chrome.projects in the executor sandbox when running in Ghost Browser. - protocol.ts: add GhostBrowserCommandMessage type - background.ts: handle ghost-browser messages, call chrome APIs - cdp-relay.ts: route ghost-browser commands to extension - executor.ts: inject chrome object with Proxy-based API namespaces - skill.md: document Ghost Browser integration with examples Enables multi-identity automation for managing multiple accounts, rotating proxies per tab/identity, and isolated cookie sessions. --- extension/src/background.ts | 52 ++++++++++++++++++++++ playwriter/src/cdp-relay.ts | 8 ++++ playwriter/src/executor.ts | 52 ++++++++++++++++++++++ playwriter/src/protocol.ts | 22 +++++++++ playwriter/src/skill.md | 89 +++++++++++++++++++++++++++++++++++++ 5 files changed, 223 insertions(+) diff --git a/extension/src/background.ts b/extension/src/background.ts index 0f632e8..97c7306 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -274,6 +274,58 @@ class ConnectionManager { return } + // Handle Ghost Browser API commands + // This allows calling chrome.ghostPublicAPI, chrome.ghostProxies, chrome.projects + // from the playwriter executor sandbox when running in Ghost Browser + if (message.method === 'ghost-browser') { + const { namespace, method, args } = message.params as { + namespace: 'ghostPublicAPI' | 'ghostProxies' | 'projects' + method: string + args: unknown[] + } + try { + const api = (chrome as any)[namespace] + if (!api) { + sendMessage({ + id: message.id, + result: { success: false, error: `chrome.${namespace} not available (not running in Ghost Browser?)` } + }) + return + } + + const fn = api[method] + if (typeof fn !== 'function') { + // Check if it's a constant/property + if (method in api) { + sendMessage({ id: message.id, result: { success: true, result: api[method] } }) + return + } + sendMessage({ + id: message.id, + result: { success: false, error: `chrome.${namespace}.${method} is not a function or property` } + }) + return + } + + // Ghost Browser APIs use callback pattern, wrap in Promise + const result = await new Promise((resolve, reject) => { + fn.call(api, ...args, (result: unknown) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)) + } else { + resolve(result) + } + }) + }) + + sendMessage({ id: message.id, result: { success: true, result } }) + } catch (error: any) { + logger.error('Ghost Browser API error:', error) + sendMessage({ id: message.id, result: { success: false, error: error.message } }) + } + return + } + const response: ExtensionResponseMessage = { id: message.id } try { response.result = await handleCommand(message as ExtensionCommandMessage) diff --git a/playwriter/src/cdp-relay.ts b/playwriter/src/cdp-relay.ts index 25e09df..b3bd5fe 100644 --- a/playwriter/src/cdp-relay.ts +++ b/playwriter/src/cdp-relay.ts @@ -439,6 +439,14 @@ export async function startPlayWriterCDPRelayServer({ }) } + // Ghost Browser API - forward to extension for chrome.ghostPublicAPI/ghostProxies/projects + case 'ghost-browser': { + return await sendToExtension({ + method: 'ghost-browser', + params + }) + } + case 'Runtime.enable': { if (!sessionId) { break diff --git a/playwriter/src/executor.ts b/playwriter/src/executor.ts index d95bd35..07d5c76 100644 --- a/playwriter/src/executor.ts +++ b/playwriter/src/executor.ts @@ -741,6 +741,56 @@ export class PlaywrightExecutor { } const self = this + // Ghost Browser API integration - creates proxy objects that mirror chrome.ghostPublicAPI, + // chrome.ghostProxies, and chrome.projects APIs. Only works when running in Ghost Browser. + // See extension/src/ghost-browser-api.d.ts for full API documentation. + const createGhostBrowserProxy = ( + namespace: 'ghostPublicAPI' | 'ghostProxies' | 'projects', + constants: Record = {} + ) => { + const sendGhostBrowserCommand = async (method: string, args: unknown[]) => { + const cdp = await getCDPSession({ page }) + const result = await cdp.send('ghost-browser' as any, { namespace, method, args }) + const typed = result as { success: boolean; result?: unknown; error?: string } + if (!typed.success) { + throw new Error(typed.error || `Ghost Browser API call failed: ${namespace}.${method}`) + } + return typed.result + } + + return new Proxy(constants, { + get(target, prop: string) { + // Return constants directly (no await needed) + if (prop in target) { + return target[prop] + } + // Return function that sends ghost-browser command + return (...args: unknown[]) => sendGhostBrowserCommand(prop, args) + } + }) + } + + // Chrome object with Ghost Browser API namespaces + // These mirror the exact shape of chrome.ghostPublicAPI, chrome.ghostProxies, chrome.projects + const chromeGhostBrowser = { + ghostPublicAPI: createGhostBrowserProxy('ghostPublicAPI', { + NEW_TEMPORARY_IDENTITY: 'OpenInNewSession', + DEFAULT_IDENTITY: '', + MAX_TEMPORARY_IDENTITIES: 25, + }), + ghostProxies: createGhostBrowserProxy('ghostProxies', { + DIRECT_PROXY: '8f513494-8cf5-41c7-b318-936392222104', + SYSTEM_PROXY: '2485b989-7ffb-4442-a45a-d7f9a10c6171', + }), + projects: createGhostBrowserProxy('projects', { + PROJECT_ID_HOME: 'f0673216-13b9-48be-aa41-90763e229e78', + PROJECT_ID_UNSAVED: 'fe061488-8a8e-40f0-9e5e-93a1a5e2c273', + SESSIONS_MAX: 25, + NEW_SESSION: 'OpenInNewSession', + GLOBAL_SESSION: '', + }), + } + let vmContextObj: any = { page, context, @@ -773,6 +823,8 @@ export class PlaywrightExecutor { }, require: this.sandboxedRequire, import: (specifier: string) => import(specifier), + // Ghost Browser API - only works in Ghost Browser, mirrors chrome.ghostPublicAPI etc + chrome: chromeGhostBrowser, ...usefulGlobals, } diff --git a/playwriter/src/protocol.ts b/playwriter/src/protocol.ts index e915f24..8233a82 100644 --- a/playwriter/src/protocol.ts +++ b/playwriter/src/protocol.ts @@ -178,3 +178,25 @@ export type CancelRecordingResult = { success: boolean error?: string } + +// Ghost Browser API command message (for Ghost Browser integration) +export type GhostBrowserCommandMessage = { + id: number + method: 'ghost-browser' + params: { + /** API namespace: 'ghostPublicAPI' | 'ghostProxies' | 'projects' */ + namespace: 'ghostPublicAPI' | 'ghostProxies' | 'projects' + /** Method name within the namespace */ + method: string + /** Arguments to pass to the method */ + args: unknown[] + } +} + +export type GhostBrowserCommandResult = { + success: true + result: unknown +} | { + success: false + error: string +} diff --git a/playwriter/src/skill.md b/playwriter/src/skill.md index 813620b..09cbf6d 100644 --- a/playwriter/src/skill.md +++ b/playwriter/src/skill.md @@ -723,6 +723,95 @@ Examples of what playwriter can do: - Record videos of browser sessions that survive page navigation +## Ghost Browser integration + +Playwriter supports [Ghost Browser](https://ghostbrowser.com/) for multi-identity automation. When running in Ghost Browser, you can control identities, proxies, and sessions programmatically via the `chrome` object in the sandbox. + +**Use cases:** +- Managing multiple social media accounts (Reddit, Twitter, etc.) +- Web scraping with rotating proxies per tab +- Testing with different user sessions simultaneously +- Automation requiring isolated cookies/storage per identity + +**API namespaces available:** +- `chrome.ghostPublicAPI` - Open tabs in specific identities +- `chrome.ghostProxies` - Manage and assign proxies per tab/identity +- `chrome.projects` - List/manage identities, sessions, workspaces + +**Example - Open tabs in different identities:** + +```js +// List all configured identities +const identities = await chrome.projects.getIdentitiesList(); +console.log(identities.map(i => ({ id: i.id, name: i.name }))); + +// Open tab in a new temporary identity +const tabId = await chrome.ghostPublicAPI.openTab({ + url: 'https://reddit.com', + identity: chrome.ghostPublicAPI.NEW_TEMPORARY_IDENTITY +}); + +// Open tab in a specific identity +await chrome.ghostPublicAPI.openTab({ + url: 'https://twitter.com', + identity: identities[0].id +}); +``` + +**Example - Set proxies per tab:** + +```js +// List all configured proxies +const proxies = await chrome.ghostProxies.getList(); +console.log(proxies.map(p => ({ id: p.id, name: p.name, uri: p.uri }))); + +// Set proxy for a specific tab (use current page's tab ID) +const tabId = 123; // get from page or chrome.tabs +await chrome.ghostProxies.setTabProxy(tabId, proxies[0].id); + +// Set proxy for an entire identity (all tabs in that identity) +await chrome.ghostProxies.setIdentityProxy(identities[0].id, proxies[0].id); + +// Use direct connection (no proxy) +await chrome.ghostProxies.setTabProxy(tabId, chrome.ghostProxies.DIRECT_PROXY); +``` + +**Example - Multi-account Reddit automation:** + +```js +// Get identities for different Reddit accounts +const identities = await chrome.projects.getIdentitiesList(); +const redditIdentities = identities.filter(i => i.name.includes('reddit')); + +// Open each account in its own identity with isolated cookies +for (const identity of redditIdentities) { + await chrome.ghostPublicAPI.openTab({ + url: 'https://reddit.com', + identity: identity.id + }); +} +``` + +**Constants:** + +```js +chrome.ghostPublicAPI.NEW_TEMPORARY_IDENTITY // "OpenInNewSession" +chrome.ghostPublicAPI.DEFAULT_IDENTITY // "" +chrome.ghostPublicAPI.MAX_TEMPORARY_IDENTITIES // 25 + +chrome.ghostProxies.DIRECT_PROXY // UUID for no proxy +chrome.ghostProxies.SYSTEM_PROXY // UUID for system proxy + +chrome.projects.SESSIONS_MAX // 25 +chrome.projects.NEW_SESSION // "OpenInNewSession" +chrome.projects.GLOBAL_SESSION // "" +``` + +For the complete API reference including all methods, parameters, and types, read the type definitions file: +`/Users/morse/Documents/GitHub/playwriter/extension/src/ghost-browser-api.d.ts` + +**Note:** These APIs only work when running Playwriter in Ghost Browser. In regular Chrome, calls will fail with "not available" errors. + ## debugging playwriter issues if some internal critical error happens you can read your own relay ws logs to understand the issue, it will show logs from extension, mcp and ws server together. then you can create a gh issue using `gh issue create -R remorses/playwriter --title title --body body`. ask for user confirmation before doing this.