diff --git a/assets/architecture.svg b/assets/architecture.svg index 9f99c11..9e64b4d 100644 --- a/assets/architecture.svg +++ b/assets/architecture.svg @@ -1,86 +1,80 @@ - - + + + + + - - + + + + + raw-browser / architecture + + + + + Agent / script + one multi-step task + written as one script + + raw run + + MCP + + + + raw runtime + Node 22+ / zero npm deps + one script, one task + page.snapshot() / waits / downloads + + + + + - - CALLER - - Agent / script - one multi-step task - written as one script - - raw run - - MCP + + + Chrome / Edge + your real, signed-in + profile, tabs and cookies + + no re-login - - RUNTIME - - raw runtime - Node 22+ · zero npm deps - worker-isolated scripts - page.* API · snapshots - timeouts · downloads + + + CDP 9222 + direct - - BROWSER - - Chrome / Edge - your real, signed-in - profile — tabs, cookies, - extensions, downloads - - no re-login + + + bridge 9333 / 9334 - - - - CDP :9222 - DIRECT MODE + + Native host + .NET 9 / stdio + 127.0.0.1 + token - - - - Native host - .NET 9 · stdio - 127.0.0.1 + token + - - - MV3 ext. - chrome.debugger - nativeMessaging + + MV3 extension + chrome.debugger + nativeMessaging - - BRIDGE MODE · :9333 / :9334 + - - - Direct mode - Own dedicated profile, cross-platform, isolated --space contexts. + + + + Direct + dedicated profile / isolated spaces / cross-platform - - Bridge mode - Drives the browser you are already signed into. Windows installer today. + + Bridge + your signed-in browser / token-authenticated loopback / Windows installer diff --git a/assets/demo.gif b/assets/demo.gif index 52c8131..0205415 100644 Binary files a/assets/demo.gif and b/assets/demo.gif differ diff --git a/assets/demo.mp4 b/assets/demo.mp4 index ad43a0d..97cda0e 100644 Binary files a/assets/demo.mp4 and b/assets/demo.mp4 differ diff --git a/assets/social-card.html b/assets/social-card.html index 5eea29a..01fbea4 100644 --- a/assets/social-card.html +++ b/assets/social-card.html @@ -4,109 +4,226 @@ raw-browser social card -
-
- raw - raw-browser/ browser automation +
+
+
raw-browser
+
your browser
+
one script
+
+
-

Your browser.
One script.

-

A zero-dependency browser automation runtime, CLI and MCP server for AI agents. Drives the Chrome or Edge you're already signed into.

-
- MIT - Node 22+ - Chrome & Edge - MCP server - 51 tests + +
+ +
raw://one-script
+ raw connected
-
github.com/liutingqiu/raw-browser
-
-
-
demo.mjs
-
const snap = await page.snapshot();
-await page.click('#12');
-await page.fill('role=textbox', 'Initech');
 
-const file = await page.waitForDownload(() =>
-  page.click('#export'));
-// orders.csv · 66 bytes · one round-trip
+
+
+

Your browser.

+
+
One script. + + +
+
1 element selected
+
+ +

A zero-dependency browser automation runtime, CLI and MCP server for AI agents. Drives the Chrome or Edge you're already signed into.

+ +
+ MIT + Node 22+ + v0.6.0 + MCP 16 tools +
+ +
› raw run demo.mjs
+
github.com/liutingqiu/raw-browser
+
+ + +
diff --git a/assets/social-card.png b/assets/social-card.png index 31ad334..fc019dd 100644 Binary files a/assets/social-card.png and b/assets/social-card.png differ diff --git a/demo/dashboard.html b/demo/dashboard.html index b6fa595..4b9ea06 100644 --- a/demo/dashboard.html +++ b/demo/dashboard.html @@ -6,234 +6,343 @@ Acme Ops — Orders -
-
- raw - Acme Ops / Orders - - live +
+
+
Acme Ops
+
raw — run.mjs
+
+
+
+ +
+ +
app.local/orders
+ raw connected
-
-
-
-

Orders

-

One script. Search, select, export.

+
+
+
+
+

Orders

+
Acme Ops / 2026-10-23
+
+ +
8orders shown
- - 8 of 8 orders -
-
- - - -
+
+ + + +
- - - - - -
OrderCustomerAmountStatusDate
-
No orders match this filter.
-
+ + + + + +
OrderCustomerAmountStatusDate
+
No orders match this filter.
-
- raw run demo.mjs - 1 script · 6 actions · 0 model round-trips - - 127.0.0.1 demo +
+ raw one script + ready + + 127.0.0.1 demo +
+ + +
-
✓Exported
-
+
+ + + +
diff --git a/scripts/demo.mjs b/scripts/demo.mjs index 9792a2c..e6f43bf 100644 --- a/scripts/demo.mjs +++ b/scripts/demo.mjs @@ -26,7 +26,7 @@ const ASSETS = path.join(ROOT, "assets"); const args = process.argv.slice(2); const headless = args.includes("--headless"); const keep = args.includes("--keep"); -const width = 1200; +const width = 1280; const height = 800; function fail(message) { @@ -69,6 +69,7 @@ await ui.cdpSend('Emulation.setDeviceMetricsOverride', { width: ${width}, height: ${height}, deviceScaleFactor: 1, mobile: false, }); await ui.sleep(450); +await ui.eval('window.__demoScript(1)'); const FRAMES = ${JSON.stringify(frames)}; const timings = []; @@ -97,7 +98,8 @@ async function moveTo(selector, steps = 9) { await snap(); return target; } -async function clickAt(selector) { +async function clickAt(selector, line) { + if (line) await ui.eval('window.__demoScript(' + line + ')'); const target = await moveTo(selector); await ui.eval('window.__demoClick(' + target.x + ',' + target.y + ')'); await ui.click(selector); @@ -108,8 +110,10 @@ await hold(10, 100); const snapshot = await ui.snapshot({ scope: 'viewport' }); const elements = snapshot.split('\\n').filter((line) => /^\\[\\d+\\]/.test(line)).length; console.log('snapshot: ' + elements + ' interactive elements, ' + snapshot.length + ' chars'); +await ui.eval('window.__demoLog(' + JSON.stringify('snapshot · ' + elements + ' interactive') + ')'); -await clickAt('input[placeholder="Search orders"]'); +await clickAt('input[placeholder="Search orders"]', 3); +await ui.eval('window.__demoScript(4)'); for (const ch of 'Initech') { await ui.typeText(ch); await sleep(85); @@ -118,17 +122,25 @@ for (const ch of 'Initech') { await ui.sleep(220); await snap(); -await clickAt('text=Initech'); +await ui.eval('window.__demoScript(6)'); +await moveTo('text=Initech'); +await ui.eval('window.__demoScript(7)'); +await ui.click('text=Initech'); +await snap(); await hold(8, 120); +await ui.eval('window.__demoScript(9)'); const exportButton = await moveTo('#export'); +await ui.eval('window.__demoScript(10)'); await ui.eval('window.__demoClick(' + exportButton.x + ',' + exportButton.y + ')'); const download = await ui.waitForDownload(() => ui.click('#export'), { timeoutMs: 20000 }); +await ui.eval('window.__demoScript(12)'); +await ui.eval('window.__demoLog(' + JSON.stringify('downloaded · ' + download.filename) + ')'); await snap(); console.log('downloaded: ' + download.filename + ' (' + download.bytes + ' bytes)'); await hold(12, 110); -await ui.eval('window.__demoCursor(1060, 700)'); +await ui.eval('window.__demoCursor(1180, 730)'); await hold(6, 120); await ui.closeTab(); console.log('TIMINGS:' + JSON.stringify(timings)); @@ -167,7 +179,7 @@ async function encode(directory, frames, timings) { await run("ffmpeg", [ "-y", "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0", "-i", listFile, - "-vf", "fps=15,scale=960:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=160[p];[b][p]paletteuse=dither=sierra2_4a", + "-vf", "fps=15,scale=1200:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=192[p];[b][p]paletteuse=dither=sierra2_4a", "-loop", "0", gif, ]); diff --git a/scripts/smoke-browser.mjs b/scripts/smoke-browser.mjs index f3c7528..10aa4b6 100644 --- a/scripts/smoke-browser.mjs +++ b/scripts/smoke-browser.mjs @@ -24,6 +24,8 @@ process.env.RAW_BROWSER_PORT = String(debugPort); process.env.RAW_BROWSER_PROFILE = profile; process.env.RAW_DOWNLOAD_DIR = downloads; process.env.RAW_BROWSER_HEADLESS = "1"; +// Windows CI runners can be slow to start a cold browser. +process.env.RAW_BROWSER_LAUNCH_TIMEOUT_MS = "60000"; const { closeBrowser, runBrowserScript } = await import("../dist/browser.js"); const html = ` @@ -121,14 +123,19 @@ async function removeTempRoot() { fs.rmSync(root, { recursive: true, force: true }); return; } catch (err) { - if (attempt === 29) throw err; + // A browser that failed to start can still hold the profile open on + // Windows. Reporting that permission error would hide the real failure. + if (attempt === 29) { + console.warn(`smoke: could not remove ${root}: ${err.message}`); + return; + } await delay(100); } } } try { - const result = await runBrowserScript(script, { timeoutMs: 30_000 }); + const result = await runBrowserScript(script, { timeoutMs: 90_000 }); console.log(result.text); assert.equal(result.text.startsWith("Error:"), false, result.text); assert.equal(result.images?.length, 2); diff --git a/src/browser.ts b/src/browser.ts index cc3bb6d..8933321 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -63,6 +63,7 @@ const BASE = `http://127.0.0.1:${PORT}`; const HEADLESS = process.env.RAW_BROWSER_HEADLESS === "1"; const PROFILE = process.env.RAW_BROWSER_PROFILE || BROWSER_PROFILE; const BROWSER_MARKER = path.join(PROFILE, ".raw-browser.json"); +const LAUNCH_TIMEOUT_MS = Number(process.env.RAW_BROWSER_LAUNCH_TIMEOUT_MS) || 30_000; function debuggerFetch(pathname: string, init: RequestInit = {}) { const headers = new Headers(init.headers); @@ -297,7 +298,7 @@ async function launchBrowser(): Promise { const child = spawn(exe, args, { detached: true, stdio: "ignore" }); child.unref(); - const deadline = Date.now() + 30_000; + const deadline = Date.now() + LAUNCH_TIMEOUT_MS; while (Date.now() < deadline) { await new Promise((r) => setTimeout(r, 400)); const info = await debuggerAlive(); @@ -313,10 +314,11 @@ async function launchBrowser(): Promise { return; } } + const seconds = Math.round(LAUNCH_TIMEOUT_MS / 1000); throw new Error( EXTERNAL_BRIDGE - ? `${BRIDGE_BROWSER} did not expose the Raw bridge on port ${PORT} within 30s. Check that the Raw Browser Bridge extension is loaded in that browser and the native host is installed.` - : `browser did not expose a debugger on port ${PORT} within 30s`, + ? `${BRIDGE_BROWSER} did not expose the Raw bridge on port ${PORT} within ${seconds}s. Check that the Raw Browser Bridge extension is loaded in that browser and the native host is installed.` + : `browser did not expose a debugger on port ${PORT} within ${seconds}s`, ); }