From 0f1e6545ec32dab4b1287c40b50f7d06a87713c4 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Fri, 13 Feb 2026 13:01:08 -0500 Subject: [PATCH 01/11] browser init --- README.md | 59 ++++ package.json | 4 +- pnpm-lock.yaml | 18 +- skills/firecrawl-cli/SKILL.md | 47 ++++ src/__tests__/commands/browser.test.ts | 132 +++++++++ src/commands/browser.ts | 367 +++++++++++++++++++++++++ src/index.ts | 224 ++++++++++++++- src/utils/browser-session.ts | 107 +++++++ 8 files changed, 946 insertions(+), 12 deletions(-) create mode 100644 src/__tests__/commands/browser.test.ts create mode 100644 src/commands/browser.ts create mode 100644 src/utils/browser-session.ts diff --git a/README.md b/README.md index 1b8a72716a..a9c9307e06 100644 --- a/README.md +++ b/README.md @@ -426,6 +426,65 @@ firecrawl agent abc123-def456-... --wait --poll-interval 10 --- +### `browser` - Cloud browser sessions (Beta) + +Launch and control cloud browser sessions. Execute Python (Playwright) code remotely — `page`, `browser`, and `context` are pre-configured. Use `print()` to return output. + +```bash +# 1. Launch a session +firecrawl browser launch --stream + +# 2. Execute Python remotely +firecrawl browser execute "await page.goto('https://example.com'); print(await page.title())" + +# 3. List sessions +firecrawl browser list + +# 4. Close +firecrawl browser close +``` + +#### Launch Options + +| Option | Description | +| ---------------------------- | ------------------------------------------- | +| `--ttl ` | Total session TTL in seconds (default: 300) | +| `--ttl-inactivity ` | Inactivity TTL in seconds | +| `--stream` | Enable live view streaming | +| `-o, --output ` | Save output to file | +| `--json` | Output as JSON format | +| `--pretty` | Pretty print JSON output | + +#### Execute Options + +| Option | Description | +| --------------------- | ------------------------------------------------ | +| `--session ` | Target a specific session (auto-saved on launch) | +| `-o, --output ` | Save output to file | +| `--json` | Output as JSON format | +| `--pretty` | Pretty print JSON output | + +#### Examples + +```bash +# Navigate and extract +firecrawl browser execute "await page.goto('https://example.com'); print(await page.title())" + +# Snapshot interactive elements +firecrawl browser execute "print(await page.locator('body').aria_snapshot())" + +# Fill a form +firecrawl browser execute "await page.get_by_role('textbox', name='Search').fill('firecrawl')" + +# Launch with extended TTL +firecrawl browser launch --ttl 900 --ttl-inactivity 120 + +# JSON output +firecrawl browser execute --json "print(await page.title())" +``` + +--- + ### `config` - Configure and view settings ```bash diff --git a/package.json b/package.json index ef524813ba..3d31cee4aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firecrawl-cli", - "version": "1.3.1", + "version": "1.4.0-beta.1", "description": "Command-line interface for Firecrawl. Scrape, crawl, and extract data from any website directly from your terminal.", "main": "dist/index.js", "bin": { @@ -64,7 +64,7 @@ "vitest": "^4.0.0" }, "dependencies": { - "@mendable/firecrawl-js": "^4.12.0", + "@mendable/firecrawl-js": "4.13.0-beta.2", "commander": "^14.0.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f61e2fd09f..3164295fe6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@mendable/firecrawl-js': - specifier: ^4.12.0 - version: 4.12.0 + specifier: 4.13.0-beta.2 + version: 4.13.0-beta.2 commander: specifier: ^14.0.2 version: 14.0.2 @@ -195,8 +195,8 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@mendable/firecrawl-js@4.12.0': - resolution: {integrity: sha512-Jjawuumet+3htp39PwwhkZhSj2ORR8Tz/HqORoFGngrB3HadMBKoX6SSPIhayRTXwNnDkaU0PWm1SUtRvPdoPw==} + '@mendable/firecrawl-js@4.13.0-beta.2': + resolution: {integrity: sha512-OOf4Xo+5Y1nK0hFX6SuGdZtVayXH8C1JRhduG7+HxBIK4E7yYm8wymcN58ESoK0X5RqtXeExVVyiTJFoaLAHQg==} engines: {node: '>=22.0.0'} '@rollup/rollup-android-arm-eabi@4.55.1': @@ -387,8 +387,8 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axios@1.13.2: - resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + axios@1.13.5: + resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -972,9 +972,9 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@mendable/firecrawl-js@4.12.0': + '@mendable/firecrawl-js@4.13.0-beta.2': dependencies: - axios: 1.13.2 + axios: 1.13.5 typescript-event-target: 1.1.2 zod: 3.25.76 zod-to-json-schema: 3.25.1(zod@3.25.76) @@ -1122,7 +1122,7 @@ snapshots: asynckit@0.4.0: {} - axios@1.13.2: + axios@1.13.5: dependencies: follow-redirects: 1.15.11 form-data: 4.0.5 diff --git a/skills/firecrawl-cli/SKILL.md b/skills/firecrawl-cli/SKILL.md index 46eba9e844..c5ceb8e658 100644 --- a/skills/firecrawl-cli/SKILL.md +++ b/skills/firecrawl-cli/SKILL.md @@ -210,6 +210,53 @@ firecrawl map https://example.com --include-subdomains -o .firecrawl/all-urls.tx - `--json` - Output as JSON - `-o, --output ` - Save to file +### Browser - Cloud browser sessions + +Launch remote Chromium sessions with full Playwright control. Sessions persist across commands -- after `launch`, subsequent `execute` calls reuse the last session automatically. + +```bash +# Launch a session (saves connection details) +firecrawl browser launch -o .firecrawl/browser-session.json --json + +# Launch with custom TTL and live view streaming +firecrawl browser launch --ttl 600 --stream -o .firecrawl/browser-session.json --json + +# Execute Python code against the session (default) +firecrawl browser execute 'await page.goto("https://example.com") +print(await page.title())' -o .firecrawl/browser-result.txt + +# Execute JavaScript instead +firecrawl browser execute --js 'await page.goto("https://example.com"); console.log(await page.title());' -o .firecrawl/browser-result.txt + +# Execute against a specific session +firecrawl browser execute --session 'print(await page.title())' -o .firecrawl/browser-result.txt + +# List active sessions +firecrawl browser list --json -o .firecrawl/browser-sessions.json + +# Close last session +firecrawl browser close + +# Close a specific session +firecrawl browser close --session +``` + +**Browser Options:** + +- `--ttl ` - Total session lifetime (default: 300, range: 30-3600) +- `--ttl-inactivity ` - Auto-close after inactivity (range: 10-3600) +- `--stream` - Enable live view streaming +- `--python` - Execute as Python (default) +- `--js` - Execute as JavaScript +- `--session ` - Target specific session (default: last launched session) +- `-o, --output ` - Save to file + +**Notes:** + +- Session auto-saves after `launch` -- no need to pass `--session` for subsequent commands +- Code receives a pre-configured `page`, `browser`, and `context` objects (no setup needed) +- Use `print()` to return output from Python execution + ## Reading Scraped Files NEVER read entire firecrawl output files at once unless explicitly asked or required - they're often 1000+ lines. Instead, use grep, head, or incremental reads. Determine values dynamically based on file size and what you're looking for. diff --git a/src/__tests__/commands/browser.test.ts b/src/__tests__/commands/browser.test.ts new file mode 100644 index 0000000000..7808b651b6 --- /dev/null +++ b/src/__tests__/commands/browser.test.ts @@ -0,0 +1,132 @@ +/** + * Browser commands – minimal viable tests + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + handleBrowserLaunch, + handleBrowserExecute, + handleBrowserList, + handleBrowserClose, +} from '../../commands/browser'; +import { getClient } from '../../utils/client'; +import { initializeConfig } from '../../utils/config'; +import { setupTest, teardownTest } from '../utils/mock-client'; + +vi.mock('child_process', () => ({ spawn: vi.fn() })); + +vi.mock('../../utils/client', async () => { + const actual = await vi.importActual('../../utils/client'); + return { ...actual, getClient: vi.fn() }; +}); + +vi.mock('../../utils/browser-session', async () => { + const actual = await vi.importActual('../../utils/browser-session'); + return { + ...actual, + saveBrowserSession: vi.fn(), + loadBrowserSession: vi.fn().mockReturnValue({ + id: 'stored-session-id', + cdpUrl: 'wss://stored', + createdAt: '2025-01-01T00:00:00Z', + }), + clearBrowserSession: vi.fn(), + getSessionId: vi.fn((override?: string) => override || 'stored-session-id'), + }; +}); + +vi.mock('../../utils/output', () => ({ writeOutput: vi.fn() })); + +const mockExit = vi + .spyOn(process, 'exit') + .mockImplementation((() => {}) as any); +const mockConsoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); +const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); + +describe('Browser Commands', () => { + let mockClient: any; + + beforeEach(() => { + setupTest(); + initializeConfig({ + apiKey: 'test-api-key', + apiUrl: 'https://api.firecrawl.dev', + }); + mockClient = { + browser: vi.fn(), + browserExecute: vi.fn(), + listBrowsers: vi.fn(), + deleteBrowser: vi.fn(), + }; + vi.mocked(getClient).mockReturnValue(mockClient as any); + }); + + afterEach(() => { + teardownTest(); + vi.clearAllMocks(); + }); + + it('launch saves session on success', async () => { + mockClient.browser.mockResolvedValue({ + success: true, + id: 'session-123', + cdpUrl: 'wss://cdp.example.com/session-123', + liveViewUrl: 'https://live.example.com/browser-id', + }); + + await handleBrowserLaunch({}); + + const { saveBrowserSession } = await import('../../utils/browser-session'); + expect(saveBrowserSession).toHaveBeenCalledWith( + expect.objectContaining({ id: 'session-123' }) + ); + }); + + it('launch exits 1 on failure', async () => { + mockClient.browser.mockResolvedValue({ + success: false, + error: 'Not authorized', + }); + + await handleBrowserLaunch({}); + + expect(mockExit).toHaveBeenCalledWith(1); + }); + + it('execute sends python code to correct session', async () => { + mockClient.browserExecute.mockResolvedValue({ + success: true, + result: 'Example Domain', + }); + + await handleBrowserExecute({ code: 'await page.title()' }); + + expect(mockClient.browserExecute).toHaveBeenCalledWith( + 'stored-session-id', + { + code: 'await page.title()', + language: 'python', + } + ); + }); + + it('list returns sessions', async () => { + mockClient.listBrowsers.mockResolvedValue({ success: true, sessions: [] }); + + await handleBrowserList({}); + + expect(mockClient.listBrowsers).toHaveBeenCalledTimes(1); + }); + + it('close deletes and clears stored session', async () => { + mockClient.deleteBrowser.mockResolvedValue({ success: true }); + + await handleBrowserClose({}); + + expect(mockClient.deleteBrowser).toHaveBeenCalledWith('stored-session-id'); + const { clearBrowserSession } = await import('../../utils/browser-session'); + expect(clearBrowserSession).toHaveBeenCalled(); + }); +}); diff --git a/src/commands/browser.ts b/src/commands/browser.ts new file mode 100644 index 0000000000..b478de385e --- /dev/null +++ b/src/commands/browser.ts @@ -0,0 +1,367 @@ +/** + * Browser command implementation + * Manages cloud browser sessions via the Firecrawl SDK + */ + +import { spawn } from 'child_process'; +import { getClient } from '../utils/client'; +import { + saveBrowserSession, + loadBrowserSession, + clearBrowserSession, + getSessionId, +} from '../utils/browser-session'; +import { writeOutput } from '../utils/output'; + +export interface BrowserLaunchOptions { + ttl?: number; + ttlInactivity?: number; + stream?: boolean; + apiKey?: string; + apiUrl?: string; + output?: string; + json?: boolean; + pretty?: boolean; +} + +export interface BrowserExecuteOptions { + code: string; + language?: 'python' | 'js' | 'bash'; + session?: string; + apiKey?: string; + apiUrl?: string; + output?: string; + json?: boolean; + pretty?: boolean; +} + +export interface BrowserListOptions { + apiKey?: string; + apiUrl?: string; + output?: string; + json?: boolean; + pretty?: boolean; +} + +export interface BrowserCloseOptions { + session?: string; + apiKey?: string; + apiUrl?: string; + output?: string; + json?: boolean; + pretty?: boolean; +} + +/** + * Launch a new browser session + */ +export async function handleBrowserLaunch( + options: BrowserLaunchOptions +): Promise { + try { + const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); + + const args: { + ttlTotal?: number; + ttlWithoutActivity?: number; + streamWebView?: boolean; + } = {}; + if (options.ttl !== undefined) args.ttlTotal = options.ttl; + if (options.ttlInactivity !== undefined) + args.ttlWithoutActivity = options.ttlInactivity; + if (options.stream !== undefined) args.streamWebView = options.stream; + + const data = await app.browser(args); + + if (!data.success) { + console.error('Error:', data.error || 'Unknown error'); + process.exit(1); + } + + // Save session for future commands + saveBrowserSession({ + id: data.id!, + cdpUrl: data.cdpUrl!, + createdAt: new Date().toISOString(), + }); + + if (options.json || options.pretty) { + const output = options.pretty + ? JSON.stringify(data, null, 2) + : JSON.stringify(data); + writeOutput(output, options.output, !!options.output); + } else { + const lines: string[] = []; + lines.push(`Session ID: ${data.id}`); + lines.push(`CDP URL: ${data.cdpUrl}`); + if ((data as unknown as Record).liveViewUrl) { + lines.push( + `Live View URL: ${(data as unknown as Record).liveViewUrl}` + ); + } + writeOutput(lines.join('\n'), options.output, !!options.output); + } + } catch (error) { + console.error( + 'Error:', + error instanceof Error ? error.message : 'Unknown error occurred' + ); + process.exit(1); + } +} + +/** + * Execute a bash command locally with CDP_URL and SESSION_ID env vars. + * Auto-injects --cdp for agent-browser commands. + */ +export function executeBashLocally( + command: string, + session: { id: string; cdpUrl: string } +): Promise<{ stdout: string; stderr: string; exitCode: number }> { + return new Promise((resolve) => { + // Auto-inject --cdp for agent-browser commands + let finalCommand = command; + if (command.startsWith('agent-browser') && !command.includes('--cdp')) { + const parts = command.split(' '); + // Insert --cdp after "agent-browser " + const insertIdx = Math.min(2, parts.length); + parts.splice(insertIdx, 0, '--cdp', `'${session.cdpUrl}'`); + finalCommand = parts.join(' '); + } + + const child = spawn('sh', ['-c', finalCommand], { + env: { + ...process.env, + CDP_URL: session.cdpUrl, + SESSION_ID: session.id, + }, + stdio: ['inherit', 'pipe', 'pipe'], + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data: Buffer) => { + stdout += data.toString(); + }); + child.stderr.on('data', (data: Buffer) => { + stderr += data.toString(); + }); + + child.on('close', (code: number | null) => { + resolve({ stdout, stderr, exitCode: code ?? 1 }); + }); + }); +} + +/** + * Detect if an error indicates an expired or destroyed browser session. + */ +function isSessionExpiredError(error: unknown): boolean { + const msg = + error instanceof Error + ? error.message.toLowerCase() + : String(error).toLowerCase(); + const status = (error as { status?: number }).status; + if (status === 410 || status === 404) return true; + return /destroyed|expired|not found|gone|session.*closed/i.test(msg); +} + +/** + * Execute code in a browser session + */ +export async function handleBrowserExecute( + options: BrowserExecuteOptions +): Promise { + // Bash execution runs locally — skip the API entirely + if (options.language === 'bash') { + try { + const session = loadBrowserSession(); + if (!session) { + console.error( + 'Error: No active browser session. Run `firecrawl browser launch` first.' + ); + process.exit(1); + return; + } + + const result = await executeBashLocally(options.code, session); + + if (result.exitCode !== 0) { + if (result.stderr) process.stderr.write(result.stderr); + if (result.stdout) process.stdout.write(result.stdout); + process.exit(1); + } + + if (options.json || options.pretty) { + const data = { + success: true, + result: result.stdout.trimEnd(), + exitCode: result.exitCode, + }; + const output = options.pretty + ? JSON.stringify(data, null, 2) + : JSON.stringify(data); + writeOutput(output, options.output, !!options.output); + } else { + if (result.stdout) { + writeOutput( + result.stdout.trimEnd(), + options.output, + !!options.output + ); + } + } + } catch (error) { + console.error( + 'Error:', + error instanceof Error ? error.message : 'Unknown error occurred' + ); + process.exit(1); + } + + return; + } + + try { + const sessionId = getSessionId(options.session); + const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); + + const data = await app.browserExecute(sessionId, { + code: options.code, + language: options.language || 'python', + }); + + if (!data.success) { + console.error('Error:', data.error || 'Unknown error'); + process.exit(1); + } + + if (data.error) { + // Execution succeeded but code had an error + process.stderr.write(`Code error: ${data.error}\n`); + } + + if (options.json || options.pretty) { + const output = options.pretty + ? JSON.stringify(data, null, 2) + : JSON.stringify(data); + writeOutput(output, options.output, !!options.output); + } else { + if (data.result !== undefined && data.result !== '') { + writeOutput(data.result, options.output, !!options.output); + } + } + } catch (error) { + if (isSessionExpiredError(error)) { + const sessionId = + options.session || loadBrowserSession()?.id || 'unknown'; + console.error( + `Error: Session ${sessionId} has expired or been destroyed.\n` + + 'The session may have exceeded its TTL or been closed.\n' + + 'Start a new session with: firecrawl browser launch' + ); + // Clear stale stored session + const stored = loadBrowserSession(); + if (stored && !options.session) { + clearBrowserSession(); + } + } else { + console.error( + 'Error:', + error instanceof Error ? error.message : 'Unknown error occurred' + ); + } + process.exit(1); + } +} + +/** + * List browser sessions + */ +export async function handleBrowserList( + options: BrowserListOptions +): Promise { + try { + const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); + + const data = await app.listBrowsers(); + + if (!data.success) { + console.error('Error:', data.error || 'Unknown error'); + process.exit(1); + } + + const sessions = data.sessions || []; + + if (options.json || options.pretty) { + const output = options.pretty + ? JSON.stringify(data, null, 2) + : JSON.stringify(data); + writeOutput(output, options.output, !!options.output); + } else { + if (sessions.length === 0) { + writeOutput( + 'No active browser sessions.', + options.output, + !!options.output + ); + } else { + const lines: string[] = []; + for (const s of sessions) { + const age = s.createdAt + ? `created ${new Date(s.createdAt).toLocaleString()}` + : ''; + lines.push(`${s.id} ${s.status} ${age}`); + } + writeOutput(lines.join('\n'), options.output, !!options.output); + } + } + } catch (error) { + console.error( + 'Error:', + error instanceof Error ? error.message : 'Unknown error occurred' + ); + process.exit(1); + } +} + +/** + * Close a browser session + */ +export async function handleBrowserClose( + options: BrowserCloseOptions +): Promise { + try { + const sessionId = getSessionId(options.session); + const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); + + const data = await app.deleteBrowser(sessionId); + + if (!data.success) { + console.error('Error:', data.error || 'Unknown error'); + process.exit(1); + } + + // Clear stored session + const stored = loadBrowserSession(); + if (stored && stored.id === sessionId) { + clearBrowserSession(); + } + + console.log(`Session closed (${sessionId})`); + + if (options.json || options.pretty) { + const output = options.pretty + ? JSON.stringify({ success: true, id: sessionId }, null, 2) + : JSON.stringify({ success: true, id: sessionId }); + writeOutput(output, options.output, !!options.output); + } + } catch (error) { + console.error( + 'Error:', + error instanceof Error ? error.message : 'Unknown error occurred' + ); + process.exit(1); + } +} diff --git a/src/index.ts b/src/index.ts index c36c9dff7d..8df8b44aca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,12 @@ import { handleCrawlCommand } from './commands/crawl'; import { handleMapCommand } from './commands/map'; import { handleSearchCommand } from './commands/search'; import { handleAgentCommand } from './commands/agent'; +import { + handleBrowserLaunch, + handleBrowserExecute, + handleBrowserList, + handleBrowserClose, +} from './commands/browser'; import { handleVersionCommand } from './commands/version'; import { handleLoginCommand } from './commands/login'; import { handleLogoutCommand } from './commands/logout'; @@ -37,6 +43,7 @@ const AUTH_REQUIRED_COMMANDS = [ 'map', 'search', 'agent', + 'browser', 'credit-usage', ]; @@ -580,11 +587,226 @@ function createAgentCommand(): Command { return agentCmd; } -// Add crawl, map, search, and agent commands to main program +/** + * Create and configure the browser command + */ +function createBrowserCommand(): Command { + const browserCmd = new Command('browser') + .description('Manage cloud browser sessions') + .addHelpText( + 'after', + ` +Examples: + $ firecrawl browser launch # Start a session + $ firecrawl browser execute 'print(await page.title())' + $ firecrawl browser close + +Quick Start: + $ firecrawl browser launch --stream + $ firecrawl browser execute 'await page.goto("https://example.com"); print(await page.title())' + $ firecrawl browser close +` + ); + + browserCmd + .command('launch') + .description('Launch a new cloud browser session') + .option( + '--ttl ', + 'Total session TTL in seconds (default: 300)', + parseInt + ) + .option('--ttl-inactivity ', 'Inactivity TTL in seconds', parseInt) + .option('--stream', 'Enable live view streaming', false) + .option( + '-k, --api-key ', + 'Firecrawl API key (overrides global --api-key)' + ) + .option('--api-url ', 'API URL (overrides global --api-url)') + .option('-o, --output ', 'Output file path (default: stdout)') + .option('--json', 'Output as JSON format', false) + .option('--pretty', 'Pretty print JSON output', false) + .addHelpText( + 'after', + ` +Output: + Prints the Session ID and CDP URL. The session is auto-saved so + subsequent execute/close commands target it automatically. + +Examples: + $ firecrawl browser launch + $ firecrawl browser launch --stream --ttl 600 + $ firecrawl browser launch --ttl 300 --ttl-inactivity 60 + $ firecrawl browser launch -o session.json --json --pretty +` + ) + .action(async (options) => { + await handleBrowserLaunch({ + ttl: options.ttl, + ttlInactivity: options.ttlInactivity, + stream: options.stream, + apiKey: options.apiKey, + apiUrl: options.apiUrl, + output: options.output, + json: options.json, + pretty: options.pretty, + }); + }); + + browserCmd + .command('execute') + .description('Execute code in a browser session') + .argument('', 'Code to execute (Playwright async code)') + .option('--python', 'Execute as Python (default)', false) + .option('--js', 'Execute as JavaScript', false) + .option( + '--bash', + 'Execute as local bash command (sets CDP_URL and SESSION_ID env vars)', + false + ) + .option( + '--session ', + 'Session ID (default: active session from last launch)' + ) + .option( + '-k, --api-key ', + 'Firecrawl API key (overrides global --api-key)' + ) + .option('--api-url ', 'API URL (overrides global --api-url)') + .option('-o, --output ', 'Output file path (default: stdout)') + .option('--json', 'Output as JSON format', false) + .option('--pretty', 'Pretty print JSON output', false) + .addHelpText( + 'after', + ` +How it works: + Code runs server-side in a sandbox with Playwright pre-configured. + A "page" object is already available — no imports or CDP connection needed. + Use "await" for all Playwright calls. Use "print()" (Python) or the last + expression value (JS) to return output. + + IMPORTANT: Do NOT import playwright or connect to CDP in your code. + The page object is already set up for you. + +Python examples (default): + $ firecrawl browser execute 'await page.goto("https://example.com")' + $ firecrawl browser execute 'print(await page.title())' + $ firecrawl browser execute ' + await page.goto("https://news.ycombinator.com") + title = await page.title() + items = await page.query_selector_all(".titleline > a") + for item in items[:5]: + print(await item.inner_text()) + ' + +JavaScript examples: + $ firecrawl browser execute --js 'await page.goto("https://example.com"); await page.title()' + +Bash examples (runs locally, CDP_URL env var set): + $ firecrawl browser execute --bash 'agent-browser snapshot' + $ firecrawl browser execute --bash 'echo $CDP_URL' + +Target a specific session: + $ firecrawl browser execute --session 'print(await page.title())' + +Note: --python, --js, and --bash are mutually exclusive. +` + ) + .action(async (code, options) => { + const flagCount = [options.python, options.js, options.bash].filter( + Boolean + ).length; + if (flagCount > 1) { + console.error( + 'Error: Only one of --python, --js, or --bash can be specified' + ); + process.exit(1); + } + const language = options.bash ? 'bash' : options.js ? 'js' : 'python'; + await handleBrowserExecute({ + code, + language, + session: options.session, + apiKey: options.apiKey, + apiUrl: options.apiUrl, + output: options.output, + json: options.json, + pretty: options.pretty, + }); + }); + + browserCmd + .command('list') + .description('List active browser sessions') + .option( + '-k, --api-key ', + 'Firecrawl API key (overrides global --api-key)' + ) + .option('--api-url ', 'API URL (overrides global --api-url)') + .option('-o, --output ', 'Output file path (default: stdout)') + .option('--json', 'Output as JSON format', false) + .option('--pretty', 'Pretty print JSON output', false) + .addHelpText( + 'after', + ` +Examples: + $ firecrawl browser list + $ firecrawl browser list --json --pretty +` + ) + .action(async (options) => { + await handleBrowserList({ + apiKey: options.apiKey, + apiUrl: options.apiUrl, + output: options.output, + json: options.json, + pretty: options.pretty, + }); + }); + + browserCmd + .command('close') + .description('Close a browser session') + .option( + '--session ', + 'Session ID (default: active session from last launch)' + ) + .option( + '-k, --api-key ', + 'Firecrawl API key (overrides global --api-key)' + ) + .option('--api-url ', 'API URL (overrides global --api-url)') + .option('-o, --output ', 'Output file path (default: stdout)') + .option('--json', 'Output as JSON format', false) + .option('--pretty', 'Pretty print JSON output', false) + .addHelpText( + 'after', + ` +Examples: + $ firecrawl browser close + $ firecrawl browser close --session +` + ) + .action(async (options) => { + await handleBrowserClose({ + session: options.session, + apiKey: options.apiKey, + apiUrl: options.apiUrl, + output: options.output, + json: options.json, + pretty: options.pretty, + }); + }); + + return browserCmd; +} + +// Add crawl, map, search, agent, and browser commands to main program program.addCommand(createCrawlCommand()); program.addCommand(createMapCommand()); program.addCommand(createSearchCommand()); program.addCommand(createAgentCommand()); +program.addCommand(createBrowserCommand()); program .command('config') diff --git a/src/utils/browser-session.ts b/src/utils/browser-session.ts new file mode 100644 index 0000000000..af4ccefb15 --- /dev/null +++ b/src/utils/browser-session.ts @@ -0,0 +1,107 @@ +/** + * Browser session persistence utility + * Stores active browser session info in platform-specific config directory + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +export interface StoredBrowserSession { + id: string; + cdpUrl: string; + createdAt: string; +} + +/** + * Get the platform-specific config directory + */ +function getConfigDir(): string { + const homeDir = os.homedir(); + const platform = os.platform(); + + switch (platform) { + case 'darwin': + return path.join( + homeDir, + 'Library', + 'Application Support', + 'firecrawl-cli' + ); + case 'win32': + return path.join(homeDir, 'AppData', 'Roaming', 'firecrawl-cli'); + default: + return path.join(homeDir, '.config', 'firecrawl-cli'); + } +} + +/** + * Get the browser session file path + */ +function getSessionPath(): string { + return path.join(getConfigDir(), 'browser-session.json'); +} + +/** + * Ensure the config directory exists + */ +function ensureConfigDir(): void { + const configDir = getConfigDir(); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true, mode: 0o700 }); + } +} + +/** + * Save active browser session to disk + */ +export function saveBrowserSession(session: StoredBrowserSession): void { + ensureConfigDir(); + const sessionPath = getSessionPath(); + fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2), 'utf-8'); +} + +/** + * Load stored browser session from disk + */ +export function loadBrowserSession(): StoredBrowserSession | null { + try { + const sessionPath = getSessionPath(); + if (!fs.existsSync(sessionPath)) { + return null; + } + const data = fs.readFileSync(sessionPath, 'utf-8'); + return JSON.parse(data) as StoredBrowserSession; + } catch { + return null; + } +} + +/** + * Clear stored browser session from disk + */ +export function clearBrowserSession(): void { + try { + const sessionPath = getSessionPath(); + if (fs.existsSync(sessionPath)) { + fs.unlinkSync(sessionPath); + } + } catch { + // Ignore errors + } +} + +/** + * Resolve session ID from override flag or stored session + */ +export function getSessionId(overrideId?: string): string { + if (overrideId) return overrideId; + + const stored = loadBrowserSession(); + if (stored) return stored.id; + + throw new Error( + 'No active browser session. Launch one with: firecrawl browser launch\n' + + 'Or specify a session ID with: --session ' + ); +} From 4c0ac338c4b06843d23c7edf5cdc715b4a697b79 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:49:49 -0500 Subject: [PATCH 02/11] improve --help --- src/index.ts | 62 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8df8b44aca..81a0c07301 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,7 +51,9 @@ const program = new Command(); program .name('firecrawl') - .description('CLI tool for Firecrawl web scraping') + .description( + 'Firecrawl CLI — scrape, crawl, search, and automate the web. Returns clean markdown, HTML, screenshots, and structured data.' + ) .version(packageJson.version) .option( '-k, --api-key ', @@ -90,7 +92,9 @@ program */ function createScrapeCommand(): Command { const scrapeCmd = new Command('scrape') - .description('Scrape a URL using Firecrawl') + .description( + 'Scrape a single URL — returns markdown (default), HTML, links, screenshots, or structured JSON' + ) .argument('[url]', 'URL to scrape') .argument( '[formats...]', @@ -177,7 +181,9 @@ program.addCommand(createScrapeCommand()); */ function createCrawlCommand(): Command { const crawlCmd = new Command('crawl') - .description('Crawl a website using Firecrawl') + .description( + 'Crawl an entire website — follows links and returns content for every page' + ) .argument('[url-or-job-id]', 'URL to crawl or job ID to check status') .option( '-u, --url ', @@ -284,7 +290,9 @@ function createCrawlCommand(): Command { */ function createMapCommand(): Command { const mapCmd = new Command('map') - .description('Map URLs on a website using Firecrawl') + .description( + 'Discover all URLs on a website quickly — returns a list of URLs without scraping content' + ) .argument('[url]', 'URL to map') .option( '-u, --url ', @@ -346,7 +354,9 @@ function createMapCommand(): Command { */ function createSearchCommand(): Command { const searchCmd = new Command('search') - .description('Search the web using Firecrawl') + .description( + 'Search the web — returns ranked results with titles, URLs, and optionally scraped content' + ) .argument('', 'Search query') .option( '--limit ', @@ -483,7 +493,9 @@ function createSearchCommand(): Command { */ function createAgentCommand(): Command { const agentCmd = new Command('agent') - .description('Run an AI agent to extract data from the web') + .description( + 'AI agent — give a natural language prompt and it autonomously searches, scrapes, and returns structured data' + ) .argument( '', 'Natural language prompt describing data to extract, or job ID to check status' @@ -592,19 +604,30 @@ function createAgentCommand(): Command { */ function createBrowserCommand(): Command { const browserCmd = new Command('browser') - .description('Manage cloud browser sessions') + .description( + 'Launch cloud browser sessions and execute Python, JavaScript, or bash code remotely via Playwright' + ) .addHelpText( 'after', ` -Examples: - $ firecrawl browser launch # Start a session - $ firecrawl browser execute 'print(await page.title())' - $ firecrawl browser close +Overview: + Each session runs a full Chromium instance in the cloud — no local browser + or driver install needed. Code runs server-side with a pre-configured + Playwright "page" object ready to use. Supports Python (default), JavaScript, + and bash (for agent-browser / CDP tools). Quick Start: - $ firecrawl browser launch --stream + $ firecrawl browser launch $ firecrawl browser execute 'await page.goto("https://example.com"); print(await page.title())' $ firecrawl browser close + +Examples: + $ firecrawl browser launch --stream # Launch with live view + $ firecrawl browser execute 'print(await page.title())' # Playwright Python + $ firecrawl browser execute --js 'await page.title()' # Playwright JS + $ firecrawl browser execute --bash "agent-browser snapshot" # bash + CDP + $ firecrawl browser list # List active sessions + $ firecrawl browser close # Close active session ` ); @@ -655,13 +678,18 @@ Examples: browserCmd .command('execute') - .description('Execute code in a browser session') - .argument('', 'Code to execute (Playwright async code)') - .option('--python', 'Execute as Python (default)', false) - .option('--js', 'Execute as JavaScript', false) + .description( + 'Execute Playwright Python/JS code or bash commands in a browser session' + ) + .argument( + '', + 'Playwright code to execute (a "page" object is pre-configured)' + ) + .option('--python', 'Execute as Playwright Python code (default)', false) + .option('--js', 'Execute as Playwright JavaScript code', false) .option( '--bash', - 'Execute as local bash command (sets CDP_URL and SESSION_ID env vars)', + 'Execute as local bash command (CDP_URL and SESSION_ID env vars injected, auto-injects --cdp for agent-browser)', false ) .option( From a634cc3a2cf4e3e6f73fdad651ea1a863b246796 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:22:51 -0500 Subject: [PATCH 03/11] Shorten CLI descriptions and reformat help --- src/index.ts | 60 +++++++++++++++++++++------------------------------- 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/src/index.ts b/src/index.ts index 81a0c07301..2e5e82b428 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,9 +51,7 @@ const program = new Command(); program .name('firecrawl') - .description( - 'Firecrawl CLI — scrape, crawl, search, and automate the web. Returns clean markdown, HTML, screenshots, and structured data.' - ) + .description('CLI tool for Firecrawl web scraping') .version(packageJson.version) .option( '-k, --api-key ', @@ -92,9 +90,7 @@ program */ function createScrapeCommand(): Command { const scrapeCmd = new Command('scrape') - .description( - 'Scrape a single URL — returns markdown (default), HTML, links, screenshots, or structured JSON' - ) + .description('Scrape a URL using Firecrawl') .argument('[url]', 'URL to scrape') .argument( '[formats...]', @@ -181,9 +177,7 @@ program.addCommand(createScrapeCommand()); */ function createCrawlCommand(): Command { const crawlCmd = new Command('crawl') - .description( - 'Crawl an entire website — follows links and returns content for every page' - ) + .description('Crawl a website using Firecrawl') .argument('[url-or-job-id]', 'URL to crawl or job ID to check status') .option( '-u, --url ', @@ -290,9 +284,7 @@ function createCrawlCommand(): Command { */ function createMapCommand(): Command { const mapCmd = new Command('map') - .description( - 'Discover all URLs on a website quickly — returns a list of URLs without scraping content' - ) + .description('Map URLs on a website using Firecrawl') .argument('[url]', 'URL to map') .option( '-u, --url ', @@ -354,9 +346,7 @@ function createMapCommand(): Command { */ function createSearchCommand(): Command { const searchCmd = new Command('search') - .description( - 'Search the web — returns ranked results with titles, URLs, and optionally scraped content' - ) + .description('Search the web using Firecrawl') .argument('', 'Search query') .option( '--limit ', @@ -493,9 +483,7 @@ function createSearchCommand(): Command { */ function createAgentCommand(): Command { const agentCmd = new Command('agent') - .description( - 'AI agent — give a natural language prompt and it autonomously searches, scrapes, and returns structured data' - ) + .description('Run an AI agent to extract data from the web') .argument( '', 'Natural language prompt describing data to extract, or job ID to check status' @@ -610,24 +598,24 @@ function createBrowserCommand(): Command { .addHelpText( 'after', ` -Overview: - Each session runs a full Chromium instance in the cloud — no local browser - or driver install needed. Code runs server-side with a pre-configured - Playwright "page" object ready to use. Supports Python (default), JavaScript, - and bash (for agent-browser / CDP tools). - -Quick Start: - $ firecrawl browser launch - $ firecrawl browser execute 'await page.goto("https://example.com"); print(await page.title())' - $ firecrawl browser close - -Examples: - $ firecrawl browser launch --stream # Launch with live view - $ firecrawl browser execute 'print(await page.title())' # Playwright Python - $ firecrawl browser execute --js 'await page.title()' # Playwright JS - $ firecrawl browser execute --bash "agent-browser snapshot" # bash + CDP - $ firecrawl browser list # List active sessions - $ firecrawl browser close # Close active session + Overview: + Each session runs a full Chromium instance in the cloud — no local browser + or driver install needed. Code runs server-side with a pre-configured + Playwright "page" object ready to use. Supports Python (default), JavaScript, + and bash (for agent-browser / CDP tools). + + Quick Start: + $ firecrawl browser launch + $ firecrawl browser execute 'await page.goto("https://example.com"); print(await page.title())' + $ firecrawl browser close + + Examples: + $ firecrawl browser launch --stream # Launch with live view + $ firecrawl browser execute 'print(await page.title())' # Playwright Python + $ firecrawl browser execute --js 'await page.title()' # Playwright JS + $ firecrawl browser execute --bash "agent-browser snapshot" # bash + CDP + $ firecrawl browser list # List active sessions + $ firecrawl browser close # Close active session ` ); From 1a4144abd07a2d8c8e9b45a9ffd902f9de7547cd Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:44:02 -0500 Subject: [PATCH 04/11] Remove --pretty flag and add list --status Remove the deprecated --pretty CLI flag and related handling throughout browser commands. When --json is used, output is now always pretty-printed (JSON.stringify(..., 2)). Add a --status option to the browser list command and pass it into listBrowsers to filter sessions by status. Clean up minor checks (e.g. liveViewUrl access) and update help examples to reflect the removed --pretty flag. --- src/commands/browser.ts | 45 +++++++++++++++-------------------------- src/index.ts | 17 ++++++---------- 2 files changed, 22 insertions(+), 40 deletions(-) diff --git a/src/commands/browser.ts b/src/commands/browser.ts index b478de385e..0fc5d07bc5 100644 --- a/src/commands/browser.ts +++ b/src/commands/browser.ts @@ -21,7 +21,6 @@ export interface BrowserLaunchOptions { apiUrl?: string; output?: string; json?: boolean; - pretty?: boolean; } export interface BrowserExecuteOptions { @@ -32,15 +31,14 @@ export interface BrowserExecuteOptions { apiUrl?: string; output?: string; json?: boolean; - pretty?: boolean; } export interface BrowserListOptions { + status?: 'active' | 'destroyed'; apiKey?: string; apiUrl?: string; output?: string; json?: boolean; - pretty?: boolean; } export interface BrowserCloseOptions { @@ -49,7 +47,6 @@ export interface BrowserCloseOptions { apiUrl?: string; output?: string; json?: boolean; - pretty?: boolean; } /** @@ -85,19 +82,15 @@ export async function handleBrowserLaunch( createdAt: new Date().toISOString(), }); - if (options.json || options.pretty) { - const output = options.pretty - ? JSON.stringify(data, null, 2) - : JSON.stringify(data); + if (options.json) { + const output = JSON.stringify(data, null, 2); writeOutput(output, options.output, !!options.output); } else { const lines: string[] = []; lines.push(`Session ID: ${data.id}`); lines.push(`CDP URL: ${data.cdpUrl}`); - if ((data as unknown as Record).liveViewUrl) { - lines.push( - `Live View URL: ${(data as unknown as Record).liveViewUrl}` - ); + if (data.liveViewUrl) { + lines.push(`Live View URL: ${data.liveViewUrl}`); } writeOutput(lines.join('\n'), options.output, !!options.output); } @@ -193,15 +186,13 @@ export async function handleBrowserExecute( process.exit(1); } - if (options.json || options.pretty) { + if (options.json) { const data = { success: true, result: result.stdout.trimEnd(), exitCode: result.exitCode, }; - const output = options.pretty - ? JSON.stringify(data, null, 2) - : JSON.stringify(data); + const output = JSON.stringify(data, null, 2); writeOutput(output, options.output, !!options.output); } else { if (result.stdout) { @@ -242,10 +233,8 @@ export async function handleBrowserExecute( process.stderr.write(`Code error: ${data.error}\n`); } - if (options.json || options.pretty) { - const output = options.pretty - ? JSON.stringify(data, null, 2) - : JSON.stringify(data); + if (options.json) { + const output = JSON.stringify(data, null, 2); writeOutput(output, options.output, !!options.output); } else { if (data.result !== undefined && data.result !== '') { @@ -285,7 +274,9 @@ export async function handleBrowserList( try { const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); - const data = await app.listBrowsers(); + const data = await app.listBrowsers( + options.status ? { status: options.status } : undefined + ); if (!data.success) { console.error('Error:', data.error || 'Unknown error'); @@ -294,10 +285,8 @@ export async function handleBrowserList( const sessions = data.sessions || []; - if (options.json || options.pretty) { - const output = options.pretty - ? JSON.stringify(data, null, 2) - : JSON.stringify(data); + if (options.json) { + const output = JSON.stringify(data, null, 2); writeOutput(output, options.output, !!options.output); } else { if (sessions.length === 0) { @@ -351,10 +340,8 @@ export async function handleBrowserClose( console.log(`Session closed (${sessionId})`); - if (options.json || options.pretty) { - const output = options.pretty - ? JSON.stringify({ success: true, id: sessionId }, null, 2) - : JSON.stringify({ success: true, id: sessionId }); + if (options.json) { + const output = JSON.stringify({ success: true, id: sessionId }, null, 2); writeOutput(output, options.output, !!options.output); } } catch (error) { diff --git a/src/index.ts b/src/index.ts index 2e5e82b428..940c2f1ea9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -636,7 +636,6 @@ function createBrowserCommand(): Command { .option('--api-url ', 'API URL (overrides global --api-url)') .option('-o, --output ', 'Output file path (default: stdout)') .option('--json', 'Output as JSON format', false) - .option('--pretty', 'Pretty print JSON output', false) .addHelpText( 'after', ` @@ -648,7 +647,7 @@ Examples: $ firecrawl browser launch $ firecrawl browser launch --stream --ttl 600 $ firecrawl browser launch --ttl 300 --ttl-inactivity 60 - $ firecrawl browser launch -o session.json --json --pretty + $ firecrawl browser launch -o session.json --json ` ) .action(async (options) => { @@ -660,7 +659,6 @@ Examples: apiUrl: options.apiUrl, output: options.output, json: options.json, - pretty: options.pretty, }); }); @@ -691,7 +689,6 @@ Examples: .option('--api-url ', 'API URL (overrides global --api-url)') .option('-o, --output ', 'Output file path (default: stdout)') .option('--json', 'Output as JSON format', false) - .option('--pretty', 'Pretty print JSON output', false) .addHelpText( 'after', ` @@ -747,36 +744,36 @@ Note: --python, --js, and --bash are mutually exclusive. apiUrl: options.apiUrl, output: options.output, json: options.json, - pretty: options.pretty, }); }); browserCmd .command('list') - .description('List active browser sessions') + .description('List browser sessions') .option( '-k, --api-key ', 'Firecrawl API key (overrides global --api-key)' ) .option('--api-url ', 'API URL (overrides global --api-url)') + .option('--status ', 'Filter by status (active, destroyed)') .option('-o, --output ', 'Output file path (default: stdout)') .option('--json', 'Output as JSON format', false) - .option('--pretty', 'Pretty print JSON output', false) .addHelpText( 'after', ` Examples: $ firecrawl browser list - $ firecrawl browser list --json --pretty + $ firecrawl browser list --status active + $ firecrawl browser list --json ` ) .action(async (options) => { await handleBrowserList({ + status: options.status, apiKey: options.apiKey, apiUrl: options.apiUrl, output: options.output, json: options.json, - pretty: options.pretty, }); }); @@ -794,7 +791,6 @@ Examples: .option('--api-url ', 'API URL (overrides global --api-url)') .option('-o, --output ', 'Output file path (default: stdout)') .option('--json', 'Output as JSON format', false) - .option('--pretty', 'Pretty print JSON output', false) .addHelpText( 'after', ` @@ -810,7 +806,6 @@ Examples: apiUrl: options.apiUrl, output: options.output, json: options.json, - pretty: options.pretty, }); }); From 07a69957ae6e17de181de82fba830f386bf688d3 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:58:29 -0500 Subject: [PATCH 05/11] Support positional status for browser list Make the browser list command accept an optional positional status argument (active|destroyed) instead of the --status option. Validate the status value and exit with an error for invalid inputs, and pass the status through to handleBrowserList. Update help text/examples to show `firecrawl browser list active` and `firecrawl browser list destroyed`. Also update SKILL.md to clarify listing all sessions and add a separate example for listing active sessions. --- skills/firecrawl-cli/SKILL.md | 5 ++++- src/index.ts | 20 ++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/skills/firecrawl-cli/SKILL.md b/skills/firecrawl-cli/SKILL.md index c5ceb8e658..572724e9ea 100644 --- a/skills/firecrawl-cli/SKILL.md +++ b/skills/firecrawl-cli/SKILL.md @@ -231,9 +231,12 @@ firecrawl browser execute --js 'await page.goto("https://example.com"); console. # Execute against a specific session firecrawl browser execute --session 'print(await page.title())' -o .firecrawl/browser-result.txt -# List active sessions +# List all sessions firecrawl browser list --json -o .firecrawl/browser-sessions.json +# List only active sessions +firecrawl browser list active --json -o .firecrawl/browser-sessions.json + # Close last session firecrawl browser close diff --git a/src/index.ts b/src/index.ts index 940c2f1ea9..63f927d921 100644 --- a/src/index.ts +++ b/src/index.ts @@ -748,14 +748,15 @@ Note: --python, --js, and --bash are mutually exclusive. }); browserCmd - .command('list') - .description('List browser sessions') + .command('list [status]') + .description( + 'List browser sessions (optionally filter by: active, destroyed)' + ) .option( '-k, --api-key ', 'Firecrawl API key (overrides global --api-key)' ) .option('--api-url ', 'API URL (overrides global --api-url)') - .option('--status ', 'Filter by status (active, destroyed)') .option('-o, --output ', 'Output file path (default: stdout)') .option('--json', 'Output as JSON format', false) .addHelpText( @@ -763,13 +764,20 @@ Note: --python, --js, and --bash are mutually exclusive. ` Examples: $ firecrawl browser list - $ firecrawl browser list --status active + $ firecrawl browser list active + $ firecrawl browser list destroyed $ firecrawl browser list --json ` ) - .action(async (options) => { + .action(async (status, options) => { + if (status && !['active', 'destroyed'].includes(status)) { + console.error( + `Error: Invalid status "${status}". Use "active" or "destroyed".` + ); + process.exit(1); + } await handleBrowserList({ - status: options.status, + status, apiKey: options.apiKey, apiUrl: options.apiUrl, output: options.output, From fe33c8a7317980c8c8d34970ef4f4c1b8c8c77a9 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:09:50 -0500 Subject: [PATCH 06/11] Add agent-browser bash mode docs Add documentation and CLI help updates to document the new bash/agent-browser workflow. skills/firecrawl-cli/SKILL.md: introduce a Bash Mode section with recommended agent-browser usage, example commands, and a core command table; clarify that agent-browser is pre-installed in sandboxes. src/index.ts: update the browser command help text and examples to highlight agent-browser usage, streamline examples, and update the --bash option description to indicate agent-browser and CDP_URL injection. These changes make it easier for AI agents and users to run browser interactions via pre-installed agent-browser tooling. --- skills/firecrawl-cli/SKILL.md | 46 ++++++++++++++++++++++++++++++++++- src/index.ts | 42 +++++++++++++++----------------- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/skills/firecrawl-cli/SKILL.md b/skills/firecrawl-cli/SKILL.md index 572724e9ea..3d2a3cbcaa 100644 --- a/skills/firecrawl-cli/SKILL.md +++ b/skills/firecrawl-cli/SKILL.md @@ -251,15 +251,59 @@ firecrawl browser close --session - `--stream` - Enable live view streaming - `--python` - Execute as Python (default) - `--js` - Execute as JavaScript +- `--bash` - Execute bash commands in the sandbox (agent-browser is pre-installed) - `--session ` - Target specific session (default: last launched session) - `-o, --output ` - Save to file **Notes:** - Session auto-saves after `launch` -- no need to pass `--session` for subsequent commands -- Code receives a pre-configured `page`, `browser`, and `context` objects (no setup needed) +- Code receives pre-configured `page`, `browser`, and `context` objects (no setup needed) - Use `print()` to return output from Python execution +### Bash Mode with agent-browser (Recommended for AI Agents) + +For AI agents, `--bash` with agent-browser is the simplest approach. agent-browser is pre-installed in every sandbox with 40+ commands. + +```bash +# Launch a session +firecrawl browser launch -o .firecrawl/browser-session.json --json + +# Navigate to a page +firecrawl browser execute --bash "agent-browser open https://example.com" + +# Snapshot: get an accessibility tree with @ref IDs +firecrawl browser execute --bash "agent-browser snapshot" + +# Interact using @ref IDs from the snapshot +firecrawl browser execute --bash "agent-browser fill @e3 'search query'" +firecrawl browser execute --bash "agent-browser click @e8" + +# Snapshot again to see updated state +firecrawl browser execute --bash "agent-browser snapshot" + +# Extract page content +firecrawl browser execute --bash "agent-browser scrape" -o .firecrawl/browser-scrape.md + +# Close +firecrawl browser close +``` + +**Core agent-browser commands:** + +| Command | Description | +| -------------------- | -------------------------------------- | +| `open ` | Navigate to a URL | +| `snapshot` | Get accessibility tree with `@ref` IDs | +| `screenshot` | Capture a PNG screenshot | +| `click <@ref>` | Click an element by ref | +| `type <@ref> ` | Type into an element | +| `fill <@ref> ` | Fill a form field (clears first) | +| `scrape` | Extract page content as markdown | +| `scroll ` | Scroll up/down/left/right | +| `wait ` | Wait for a duration | +| `eval ` | Evaluate JavaScript on the page | + ## Reading Scraped Files NEVER read entire firecrawl output files at once unless explicitly asked or required - they're often 1000+ lines. Instead, use grep, head, or incremental reads. Determine values dynamically based on file size and what you're looking for. diff --git a/src/index.ts b/src/index.ts index 63f927d921..4bbf03f3e3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -598,24 +598,22 @@ function createBrowserCommand(): Command { .addHelpText( 'after', ` - Overview: - Each session runs a full Chromium instance in the cloud — no local browser - or driver install needed. Code runs server-side with a pre-configured - Playwright "page" object ready to use. Supports Python (default), JavaScript, - and bash (for agent-browser / CDP tools). - - Quick Start: - $ firecrawl browser launch - $ firecrawl browser execute 'await page.goto("https://example.com"); print(await page.title())' - $ firecrawl browser close - - Examples: - $ firecrawl browser launch --stream # Launch with live view - $ firecrawl browser execute 'print(await page.title())' # Playwright Python - $ firecrawl browser execute --js 'await page.title()' # Playwright JS - $ firecrawl browser execute --bash "agent-browser snapshot" # bash + CDP - $ firecrawl browser list # List active sessions - $ firecrawl browser close # Close active session +Examples: + $ firecrawl browser launch + $ firecrawl browser execute 'print(await page.title())' + $ firecrawl browser execute --bash "agent-browser snapshot" + $ firecrawl browser list active + $ firecrawl browser close + +agent-browser (pre-installed in every sandbox): + Use --bash to run agent-browser commands instead of writing Playwright code. + $ firecrawl browser execute --bash "agent-browser open https://example.com" + $ firecrawl browser execute --bash "agent-browser snapshot" + $ firecrawl browser execute --bash "agent-browser click @e5" + $ firecrawl browser execute --bash "agent-browser scrape" + + See all agent-browser commands: + $ firecrawl browser execute --bash "agent-browser --help" ` ); @@ -665,7 +663,7 @@ Examples: browserCmd .command('execute') .description( - 'Execute Playwright Python/JS code or bash commands in a browser session' + 'Execute Playwright Python/JS or bash with agent-browser in a session' ) .argument( '', @@ -675,7 +673,7 @@ Examples: .option('--js', 'Execute as Playwright JavaScript code', false) .option( '--bash', - 'Execute as local bash command (CDP_URL and SESSION_ID env vars injected, auto-injects --cdp for agent-browser)', + 'Execute bash in the sandbox (agent-browser pre-installed, CDP_URL auto-injected)', false ) .option( @@ -715,9 +713,9 @@ Python examples (default): JavaScript examples: $ firecrawl browser execute --js 'await page.goto("https://example.com"); await page.title()' -Bash examples (runs locally, CDP_URL env var set): +Bash examples (agent-browser pre-installed in sandbox): $ firecrawl browser execute --bash 'agent-browser snapshot' - $ firecrawl browser execute --bash 'echo $CDP_URL' + $ firecrawl browser execute --bash 'agent-browser open https://example.com' Target a specific session: $ firecrawl browser execute --session 'print(await page.title())' From cffb3104b089925599e61586b1b92aec696e0361 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:00:48 -0500 Subject: [PATCH 07/11] Bump firecrawl-js; update browser execute behavior Upgrade @mendable/firecrawl-js to 4.13.0-beta.3 and update CLI browser behavior. Change language handling to use 'node' (instead of 'js') and add automatic agent-browser prefixing for default commands; --python/--js now explicitly select Playwright modes. Improve session handling for bash execution by allowing a session ID to be resolved via the API if not stored locally, and rename TTL args mapping (ttlTotal -> ttl, ttlWithoutActivity -> activityTtl). Update help text and examples to reflect the new defaults. --- package.json | 2 +- pnpm-lock.yaml | 10 ++--- src/commands/browser.ts | 41 ++++++++++++++++--- src/index.ts | 91 ++++++++++++++++++++++++----------------- 4 files changed, 95 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 3d31cee4aa..c5e7e7e364 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "vitest": "^4.0.0" }, "dependencies": { - "@mendable/firecrawl-js": "4.13.0-beta.2", + "@mendable/firecrawl-js": "4.13.0-beta.3", "commander": "^14.0.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3164295fe6..25a9dd195c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@mendable/firecrawl-js': - specifier: 4.13.0-beta.2 - version: 4.13.0-beta.2 + specifier: 4.13.0-beta.3 + version: 4.13.0-beta.3 commander: specifier: ^14.0.2 version: 14.0.2 @@ -195,8 +195,8 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@mendable/firecrawl-js@4.13.0-beta.2': - resolution: {integrity: sha512-OOf4Xo+5Y1nK0hFX6SuGdZtVayXH8C1JRhduG7+HxBIK4E7yYm8wymcN58ESoK0X5RqtXeExVVyiTJFoaLAHQg==} + '@mendable/firecrawl-js@4.13.0-beta.3': + resolution: {integrity: sha512-OD8PmuQ1HdOanLub2xXVg4uCq1rJndlb/yuRYO1tUn697+ckgp0E/4yk3V49YoQM2ArW3/lIHCOKR/ifxp2ADg==} engines: {node: '>=22.0.0'} '@rollup/rollup-android-arm-eabi@4.55.1': @@ -972,7 +972,7 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@mendable/firecrawl-js@4.13.0-beta.2': + '@mendable/firecrawl-js@4.13.0-beta.3': dependencies: axios: 1.13.5 typescript-event-target: 1.1.2 diff --git a/src/commands/browser.ts b/src/commands/browser.ts index 0fc5d07bc5..51dd098a64 100644 --- a/src/commands/browser.ts +++ b/src/commands/browser.ts @@ -25,7 +25,7 @@ export interface BrowserLaunchOptions { export interface BrowserExecuteOptions { code: string; - language?: 'python' | 'js' | 'bash'; + language?: 'python' | 'node' | 'bash'; session?: string; apiKey?: string; apiUrl?: string; @@ -59,13 +59,13 @@ export async function handleBrowserLaunch( const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); const args: { - ttlTotal?: number; - ttlWithoutActivity?: number; + ttl?: number; + activityTtl?: number; streamWebView?: boolean; } = {}; - if (options.ttl !== undefined) args.ttlTotal = options.ttl; + if (options.ttl !== undefined) args.ttl = options.ttl; if (options.ttlInactivity !== undefined) - args.ttlWithoutActivity = options.ttlInactivity; + args.activityTtl = options.ttlInactivity; if (options.stream !== undefined) args.streamWebView = options.stream; const data = await app.browser(args); @@ -169,7 +169,36 @@ export async function handleBrowserExecute( // Bash execution runs locally — skip the API entirely if (options.language === 'bash') { try { - const session = loadBrowserSession(); + let session: { id: string; cdpUrl: string } | null = null; + + if (options.session) { + const stored = loadBrowserSession(); + if (stored && stored.id === options.session) { + session = stored; + } else { + // Fetch CDP URL from the API for the specified session + const app = getClient({ + apiKey: options.apiKey, + apiUrl: options.apiUrl, + }); + const list = await app.listBrowsers({ status: 'active' }); + const match = list.sessions?.find( + (s: { id: string }) => s.id === options.session + ); + if (match && match.cdpUrl) { + session = { id: match.id, cdpUrl: match.cdpUrl }; + } else { + console.error( + `Error: Session ${options.session} not found or not active.` + ); + process.exit(1); + return; + } + } + } else { + session = loadBrowserSession(); + } + if (!session) { console.error( 'Error: No active browser session. Run `firecrawl browser launch` first.' diff --git a/src/index.ts b/src/index.ts index 4bbf03f3e3..9a3c64cf48 100644 --- a/src/index.ts +++ b/src/index.ts @@ -600,20 +600,20 @@ function createBrowserCommand(): Command { ` Examples: $ firecrawl browser launch - $ firecrawl browser execute 'print(await page.title())' - $ firecrawl browser execute --bash "agent-browser snapshot" + $ firecrawl browser execute "open https://example.com" + $ firecrawl browser execute "snapshot" + $ firecrawl browser execute "click @e5" + $ firecrawl browser execute "scrape" $ firecrawl browser list active $ firecrawl browser close -agent-browser (pre-installed in every sandbox): - Use --bash to run agent-browser commands instead of writing Playwright code. - $ firecrawl browser execute --bash "agent-browser open https://example.com" - $ firecrawl browser execute --bash "agent-browser snapshot" - $ firecrawl browser execute --bash "agent-browser click @e5" - $ firecrawl browser execute --bash "agent-browser scrape" + By default, commands are sent to agent-browser (pre-installed in every sandbox). + Use --python or --node to run Playwright code instead. + $ firecrawl browser execute --python 'print(await page.title())' + $ firecrawl browser execute --node 'await page.title()' See all agent-browser commands: - $ firecrawl browser execute --bash "agent-browser --help" + $ firecrawl browser execute "--help" ` ); @@ -663,14 +663,14 @@ Examples: browserCmd .command('execute') .description( - 'Execute Playwright Python/JS or bash with agent-browser in a session' + 'Execute agent-browser commands (default), or Playwright Python/JS in a session' ) .argument( '', - 'Playwright code to execute (a "page" object is pre-configured)' + 'agent-browser command (default) or Playwright code (with --python/--node)' ) - .option('--python', 'Execute as Playwright Python code (default)', false) - .option('--js', 'Execute as Playwright JavaScript code', false) + .option('--python', 'Execute as Playwright Python code', false) + .option('--node', 'Execute as Playwright JavaScript code', false) .option( '--bash', 'Execute bash in the sandbox (agent-browser pre-installed, CDP_URL auto-injected)', @@ -691,18 +691,24 @@ Examples: 'after', ` How it works: - Code runs server-side in a sandbox with Playwright pre-configured. - A "page" object is already available — no imports or CDP connection needed. - Use "await" for all Playwright calls. Use "print()" (Python) or the last - expression value (JS) to return output. - - IMPORTANT: Do NOT import playwright or connect to CDP in your code. - The page object is already set up for you. - -Python examples (default): - $ firecrawl browser execute 'await page.goto("https://example.com")' - $ firecrawl browser execute 'print(await page.title())' - $ firecrawl browser execute ' + By default, commands are sent to agent-browser (pre-installed in every sandbox). + You don't need to type "agent-browser" — it's added automatically. + +agent-browser examples (default): + $ firecrawl browser execute "open https://example.com" + $ firecrawl browser execute "snapshot" + $ firecrawl browser execute "click @e5" + $ firecrawl browser execute "scrape" + + You can still pass the full command if you prefer: + $ firecrawl browser execute "agent-browser snapshot" + + Use --bash for arbitrary bash commands (not just agent-browser): + $ firecrawl browser execute --bash 'ls /tmp' + +Python examples (use --python): + $ firecrawl browser execute --python 'print(await page.title())' + $ firecrawl browser execute --python ' await page.goto("https://news.ycombinator.com") title = await page.title() items = await page.query_selector_all(".titleline > a") @@ -710,32 +716,43 @@ Python examples (default): print(await item.inner_text()) ' -JavaScript examples: - $ firecrawl browser execute --js 'await page.goto("https://example.com"); await page.title()' - -Bash examples (agent-browser pre-installed in sandbox): - $ firecrawl browser execute --bash 'agent-browser snapshot' - $ firecrawl browser execute --bash 'agent-browser open https://example.com' +JavaScript examples (use --node): + $ firecrawl browser execute --node 'await page.goto("https://example.com"); await page.title()' Target a specific session: - $ firecrawl browser execute --session 'print(await page.title())' + $ firecrawl browser execute --session "snapshot" -Note: --python, --js, and --bash are mutually exclusive. +Note: --python, --node, and --bash are mutually exclusive. ` ) .action(async (code, options) => { - const flagCount = [options.python, options.js, options.bash].filter( + const flagCount = [options.python, options.node, options.bash].filter( Boolean ).length; if (flagCount > 1) { console.error( - 'Error: Only one of --python, --js, or --bash can be specified' + 'Error: Only one of --python, --node, or --bash can be specified' ); process.exit(1); } - const language = options.bash ? 'bash' : options.js ? 'js' : 'python'; + const language = options.python + ? 'python' + : options.node + ? 'node' + : 'bash'; + + // In default/bash mode, auto-prefix "agent-browser" if not already present + let finalCode = code; + if ( + language === 'bash' && + !options.bash && + !finalCode.startsWith('agent-browser') + ) { + finalCode = `agent-browser ${finalCode}`; + } + await handleBrowserExecute({ - code, + code: finalCode, language, session: options.session, apiKey: options.apiKey, From ab49da2c21ca97603bb3a152da5e5a7fe8a6f5c7 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:20:51 -0500 Subject: [PATCH 08/11] docs: update CLI docs for v1.4.0 --- README.md | 123 +++++++++++++++++------------ package.json | 2 +- skills/firecrawl-cli/SKILL.md | 140 ++++++++++++++++++++++++++++++---- 3 files changed, 204 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index a9c9307e06..8303532e70 100644 --- a/README.md +++ b/README.md @@ -104,29 +104,37 @@ firecrawl https://example.com --format json -o data.json --pretty #### Scrape Options -| Option | Description | -| ------------------------ | ------------------------------------------------------- | -| `-f, --format ` | Output format(s), comma-separated | -| `-H, --html` | Shortcut for `--format html` | -| `--only-main-content` | Extract only main content (removes navs, footers, etc.) | -| `--wait-for ` | Wait time before scraping (for JS-rendered content) | -| `--screenshot` | Take a screenshot | -| `--include-tags ` | Only include specific HTML tags | -| `--exclude-tags ` | Exclude specific HTML tags | -| `-o, --output ` | Save output to file | -| `--pretty` | Pretty print JSON output | -| `--timing` | Show request timing info | +| Option | Description | +| -------------------------- | ------------------------------------------------------- | +| `-f, --format ` | Output format(s), comma-separated | +| `-H, --html` | Shortcut for `--format html` | +| `-S, --summary` | Shortcut for `--format summary` | +| `--only-main-content` | Extract only main content (removes navs, footers, etc.) | +| `--wait-for ` | Wait time before scraping (for JS-rendered content) | +| `--screenshot` | Take a screenshot | +| `--include-tags ` | Only include specific HTML tags | +| `--exclude-tags ` | Exclude specific HTML tags | +| `--max-age ` | Maximum age of cached content in milliseconds | +| `-o, --output ` | Save output to file | +| `--json` | Output as JSON format | +| `--pretty` | Pretty print JSON output | +| `--timing` | Show request timing info | #### Available Formats -| Format | Description | -| ------------ | -------------------------- | -| `markdown` | Clean markdown (default) | -| `html` | Cleaned HTML | -| `rawHtml` | Original HTML | -| `links` | All links on the page | -| `screenshot` | Screenshot as base64 | -| `json` | Structured JSON extraction | +| Format | Description | +| ---------------- | ---------------------------- | +| `markdown` | Clean markdown (default) | +| `html` | Cleaned HTML | +| `rawHtml` | Original HTML | +| `links` | All links on the page | +| `images` | All images on the page | +| `screenshot` | Screenshot as base64 | +| `summary` | AI-generated summary | +| `json` | Structured JSON extraction | +| `changeTracking` | Track changes on the page | +| `attributes` | Page attributes and metadata | +| `branding` | Brand identity extraction | #### Examples @@ -209,7 +217,7 @@ firecrawl search "web scraping" | `--scrape-formats ` | Scrape formats when `--scrape` enabled (default: markdown) | | `--only-main-content` | Include only main content when scraping (default: true) | | `-o, --output ` | Save to file | -| `--json` | Output as compact JSON (use `-p` for pretty JSON) | +| `--json` | Output as compact JSON | #### Examples @@ -221,10 +229,10 @@ firecrawl search "React Server Components" --tbs qdr:m --limit 10 firecrawl search "web scraping library" --categories github --limit 20 # Search and get full content -firecrawl search "firecrawl documentation" --scrape --scrape-formats markdown -p -o results.json +firecrawl search "firecrawl documentation" --scrape --scrape-formats markdown --json -o results.json # Find research papers -firecrawl search "large language models" --categories research -p +firecrawl search "large language models" --categories research --json # Search with location targeting firecrawl search "best coffee shops" --location "Berlin,Germany" --country DE @@ -428,19 +436,26 @@ firecrawl agent abc123-def456-... --wait --poll-interval 10 ### `browser` - Cloud browser sessions (Beta) -Launch and control cloud browser sessions. Execute Python (Playwright) code remotely — `page`, `browser`, and `context` are pre-configured. Use `print()` to return output. +Launch and control cloud browser sessions. By default, commands are sent to agent-browser (pre-installed in every sandbox). Use `--python` or `--node` to run Playwright code directly instead. ```bash # 1. Launch a session firecrawl browser launch --stream -# 2. Execute Python remotely -firecrawl browser execute "await page.goto('https://example.com'); print(await page.title())" +# 2. Execute agent-browser commands (default) +firecrawl browser execute "open https://example.com" +firecrawl browser execute "snapshot" +firecrawl browser execute "click @e5" +firecrawl browser execute "scrape" -# 3. List sessions +# 3. Execute Playwright Python or JavaScript +firecrawl browser execute --python "await page.goto('https://example.com'); print(await page.title())" +firecrawl browser execute --node "await page.goto('https://example.com'); await page.title()" + +# 4. List sessions firecrawl browser list -# 4. Close +# 5. Close firecrawl browser close ``` @@ -453,49 +468,63 @@ firecrawl browser close | `--stream` | Enable live view streaming | | `-o, --output ` | Save output to file | | `--json` | Output as JSON format | -| `--pretty` | Pretty print JSON output | #### Execute Options -| Option | Description | -| --------------------- | ------------------------------------------------ | -| `--session ` | Target a specific session (auto-saved on launch) | -| `-o, --output ` | Save output to file | -| `--json` | Output as JSON format | -| `--pretty` | Pretty print JSON output | +| Option | Description | +| --------------------- | ------------------------------------------------------------------ | +| `--python` | Execute as Playwright Python code | +| `--node` | Execute as Playwright JavaScript code | +| `--bash` | Execute bash commands in the sandbox (agent-browser pre-installed) | +| `--session ` | Target a specific session (auto-saved on launch) | +| `-o, --output ` | Save output to file | +| `--json` | Output as JSON format | + +By default (no flag), commands are sent to agent-browser. `--python`, `--node`, and `--bash` are mutually exclusive. #### Examples ```bash -# Navigate and extract -firecrawl browser execute "await page.goto('https://example.com'); print(await page.title())" +# agent-browser commands (default mode) +firecrawl browser execute "open https://example.com" +firecrawl browser execute "snapshot" +firecrawl browser execute "click @e5" +firecrawl browser execute "fill @e3 'search query'" +firecrawl browser execute "scrape" -# Snapshot interactive elements -firecrawl browser execute "print(await page.locator('body').aria_snapshot())" +# Playwright Python +firecrawl browser execute --python "await page.goto('https://example.com'); print(await page.title())" -# Fill a form -firecrawl browser execute "await page.get_by_role('textbox', name='Search').fill('firecrawl')" +# Playwright JavaScript +firecrawl browser execute --node "await page.goto('https://example.com'); await page.title()" + +# Bash (arbitrary commands in the sandbox) +firecrawl browser execute --bash "ls /tmp" # Launch with extended TTL firecrawl browser launch --ttl 900 --ttl-inactivity 120 # JSON output -firecrawl browser execute --json "print(await page.title())" +firecrawl browser execute --json "snapshot" ``` --- -### `config` - Configure and view settings +### `config` - Configure settings ```bash -# View current configuration -firecrawl config - # Configure with custom API URL firecrawl config --api-url https://firecrawl.mycompany.com firecrawl config --api-url http://localhost:3002 --api-key fc-xxx ``` +### `view-config` - View current configuration + +```bash +# View current configuration and authentication status +firecrawl view-config +``` + Shows authentication status and stored credentials location. --- @@ -538,7 +567,7 @@ firecrawl --status ``` ``` - 🔥 firecrawl cli v1.0.2 + 🔥 firecrawl cli v1.4.0 ● Authenticated via stored credentials Concurrency: 0/100 jobs (parallel scrape limit) diff --git a/package.json b/package.json index c5e7e7e364..1a27d533df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firecrawl-cli", - "version": "1.4.0-beta.1", + "version": "1.4.0", "description": "Command-line interface for Firecrawl. Scrape, crawl, and extract data from any website directly from your terminal.", "main": "dist/index.js", "bin": { diff --git a/skills/firecrawl-cli/SKILL.md b/skills/firecrawl-cli/SKILL.md index 3d2a3cbcaa..db137e412b 100644 --- a/skills/firecrawl-cli/SKILL.md +++ b/skills/firecrawl-cli/SKILL.md @@ -32,7 +32,7 @@ firecrawl --status Output when ready: ``` - 🔥 firecrawl cli v1.0.2 + 🔥 firecrawl cli v1.4.0 ● Authenticated via FIRECRAWL_API_KEY Concurrency: 0/100 jobs (parallel scrape limit) @@ -212,7 +212,7 @@ firecrawl map https://example.com --include-subdomains -o .firecrawl/all-urls.tx ### Browser - Cloud browser sessions -Launch remote Chromium sessions with full Playwright control. Sessions persist across commands -- after `launch`, subsequent `execute` calls reuse the last session automatically. +Launch remote Chromium sessions. By default, commands are sent to agent-browser (pre-installed in every sandbox). Use `--python` or `--node` to run Playwright code directly instead. Sessions persist across commands -- after `launch`, subsequent `execute` calls reuse the last session automatically. ```bash # Launch a session (saves connection details) @@ -221,15 +221,24 @@ firecrawl browser launch -o .firecrawl/browser-session.json --json # Launch with custom TTL and live view streaming firecrawl browser launch --ttl 600 --stream -o .firecrawl/browser-session.json --json -# Execute Python code against the session (default) -firecrawl browser execute 'await page.goto("https://example.com") +# Execute agent-browser commands (default mode) +firecrawl browser execute "open https://example.com" -o .firecrawl/browser-result.txt +firecrawl browser execute "snapshot" -o .firecrawl/browser-result.txt +firecrawl browser execute "click @e5" +firecrawl browser execute "scrape" -o .firecrawl/browser-scrape.md + +# Execute Playwright Python +firecrawl browser execute --python 'await page.goto("https://example.com") print(await page.title())' -o .firecrawl/browser-result.txt -# Execute JavaScript instead -firecrawl browser execute --js 'await page.goto("https://example.com"); console.log(await page.title());' -o .firecrawl/browser-result.txt +# Execute Playwright JavaScript +firecrawl browser execute --node 'await page.goto("https://example.com"); await page.title()' -o .firecrawl/browser-result.txt + +# Execute arbitrary bash in the sandbox +firecrawl browser execute --bash 'ls /tmp' -o .firecrawl/browser-result.txt # Execute against a specific session -firecrawl browser execute --session 'print(await page.title())' -o .firecrawl/browser-result.txt +firecrawl browser execute --session "snapshot" -o .firecrawl/browser-result.txt # List all sessions firecrawl browser list --json -o .firecrawl/browser-sessions.json @@ -246,21 +255,126 @@ firecrawl browser close --session **Browser Options:** -- `--ttl ` - Total session lifetime (default: 300, range: 30-3600) -- `--ttl-inactivity ` - Auto-close after inactivity (range: 10-3600) +- `--ttl ` - Total session lifetime (default: 300) +- `--ttl-inactivity ` - Auto-close after inactivity - `--stream` - Enable live view streaming -- `--python` - Execute as Python (default) -- `--js` - Execute as JavaScript -- `--bash` - Execute bash commands in the sandbox (agent-browser is pre-installed) +- `--python` - Execute as Playwright Python code +- `--node` - Execute as Playwright JavaScript code +- `--bash` - Execute bash commands in the sandbox (agent-browser pre-installed, CDP_URL auto-injected) - `--session ` - Target specific session (default: last launched session) - `-o, --output ` - Save to file +By default (no flag), commands are sent to agent-browser. `--python`, `--node`, and `--bash` are mutually exclusive. + **Notes:** - Session auto-saves after `launch` -- no need to pass `--session` for subsequent commands -- Code receives pre-configured `page`, `browser`, and `context` objects (no setup needed) +- In Python/Node mode, `page`, `browser`, and `context` objects are pre-configured (no setup needed) - Use `print()` to return output from Python execution +### Crawl - Crawl an entire website + +```bash +# Start a crawl (returns job ID) +firecrawl crawl https://example.com -o .firecrawl/crawl-result.json + +# Wait for crawl to complete +firecrawl crawl https://example.com --wait -o .firecrawl/crawl-result.json --pretty + +# With progress indicator +firecrawl crawl https://example.com --wait --progress -o .firecrawl/crawl-result.json + +# Check crawl status +firecrawl crawl + +# Limit pages and depth +firecrawl crawl https://example.com --limit 100 --max-depth 3 --wait -o .firecrawl/crawl-result.json + +# Crawl specific sections only +firecrawl crawl https://example.com --include-paths /blog,/docs --wait -o .firecrawl/crawl-blog.json + +# Exclude pages +firecrawl crawl https://example.com --exclude-paths /admin,/login --wait -o .firecrawl/crawl-result.json + +# Rate-limited crawl +firecrawl crawl https://example.com --delay 1000 --max-concurrency 2 --wait -o .firecrawl/crawl-result.json +``` + +**Crawl Options:** + +- `--wait` - Wait for crawl to complete before returning results +- `--progress` - Show progress while waiting +- `--limit ` - Maximum pages to crawl +- `--max-depth ` - Maximum crawl depth +- `--include-paths ` - Only crawl matching paths (comma-separated) +- `--exclude-paths ` - Skip matching paths (comma-separated) +- `--sitemap ` - include, skip, or only +- `--allow-subdomains` - Include subdomains +- `--allow-external-links` - Follow external links +- `--crawl-entire-domain` - Crawl entire domain +- `--ignore-query-parameters` - Treat URLs with different params as same +- `--delay ` - Delay between requests +- `--max-concurrency ` - Max concurrent requests +- `--poll-interval ` - Status check interval when waiting +- `--timeout ` - Timeout when waiting +- `-o, --output ` - Save to file +- `--pretty` - Pretty print JSON output + +### Agent - AI-powered web data extraction + +Run an AI agent that autonomously browses and extracts structured data from the web. Agent tasks typically take 2 to 5 minutes. + +```bash +# Basic usage (returns job ID immediately) +firecrawl agent "Find the pricing plans for Firecrawl" -o .firecrawl/agent-pricing.json + +# Wait for completion +firecrawl agent "Extract all product names and prices" --wait -o .firecrawl/agent-products.json + +# Focus on specific URLs +firecrawl agent "Get the main features listed" --urls https://example.com/features --wait -o .firecrawl/agent-features.json + +# Use structured output with JSON schema +firecrawl agent "Extract company info" --schema '{"type":"object","properties":{"name":{"type":"string"},"employees":{"type":"number"}}}' --wait -o .firecrawl/agent-company.json + +# Load schema from file +firecrawl agent "Extract product data" --schema-file ./product-schema.json --wait -o .firecrawl/agent-products.json + +# Use higher accuracy model +firecrawl agent "Extract detailed specs" --model spark-1-pro --wait -o .firecrawl/agent-specs.json + +# Limit cost +firecrawl agent "Get all blog post titles" --urls https://blog.example.com --max-credits 100 --wait -o .firecrawl/agent-blog.json + +# Check status of an existing job +firecrawl agent +firecrawl agent --wait +``` + +**Agent Options:** + +- `--urls ` - Comma-separated URLs to focus extraction on +- `--model ` - spark-1-mini (default, cheaper) or spark-1-pro (higher accuracy) +- `--schema ` - JSON schema for structured output (inline JSON string) +- `--schema-file ` - Path to JSON schema file +- `--max-credits ` - Maximum credits to spend (job fails if exceeded) +- `--wait` - Wait for agent to complete +- `--poll-interval ` - Polling interval when waiting (default: 5) +- `--timeout ` - Timeout when waiting +- `-o, --output ` - Save to file +- `--json` - Output as JSON format +- `--pretty` - Pretty print JSON output + +### Credit Usage - Check your credits + +```bash +# Show credit usage (human-readable) +firecrawl credit-usage + +# Output as JSON +firecrawl credit-usage --json --pretty -o .firecrawl/credits.json +``` + ### Bash Mode with agent-browser (Recommended for AI Agents) For AI agents, `--bash` with agent-browser is the simplest approach. agent-browser is pre-installed in every sandbox with 40+ commands. From 4299b5da1a1fbf25a69845e3006754ba1861c22f Mon Sep 17 00:00:00 2001 From: Nicolas <20311743+nickscamara@users.noreply.github.com> Date: Mon, 16 Feb 2026 17:30:00 -0800 Subject: [PATCH 09/11] Update SKILL.md --- skills/firecrawl-cli/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/firecrawl-cli/SKILL.md b/skills/firecrawl-cli/SKILL.md index db137e412b..5c6eb6cb83 100644 --- a/skills/firecrawl-cli/SKILL.md +++ b/skills/firecrawl-cli/SKILL.md @@ -212,7 +212,7 @@ firecrawl map https://example.com --include-subdomains -o .firecrawl/all-urls.tx ### Browser - Cloud browser sessions -Launch remote Chromium sessions. By default, commands are sent to agent-browser (pre-installed in every sandbox). Use `--python` or `--node` to run Playwright code directly instead. Sessions persist across commands -- after `launch`, subsequent `execute` calls reuse the last session automatically. +Launch remote Chromium sessions with a built-in sandbox. By default, commands are sent to agent-browser (pre-installed in every sandbox). Use `--python` or `--node` to run Playwright code directly instead. Sessions persist across commands -- after `launch`, subsequent `execute` calls reuse the last session automatically. ```bash # Launch a session (saves connection details) From dc07be1be875c3e5f4000f0c34013e3f2e0bf5bc Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:06:20 -0500 Subject: [PATCH 10/11] Add browser quick-execute and CLI shorthand Introduce handleBrowserQuickExecute to auto-launch a browser session if none exists, save the session, print session info to stderr, auto-prefix commands with "agent-browser", and delegate execution to the existing execute handler. Wire a positional [code] argument and options into the browser CLI to support the one-step shorthand (launch+execute), and rename the existing "launch" subcommand to "launch-session" while updating help/examples. Tests were added to cover quick-execute behavior (skips launch when a session exists; auto-launches and saves when not). --- src/__tests__/commands/browser.test.ts | 34 ++++++++++++++ src/commands/browser.ts | 61 ++++++++++++++++++++++++++ src/index.ts | 50 ++++++++++++++++----- 3 files changed, 134 insertions(+), 11 deletions(-) diff --git a/src/__tests__/commands/browser.test.ts b/src/__tests__/commands/browser.test.ts index 7808b651b6..881dea1ba8 100644 --- a/src/__tests__/commands/browser.test.ts +++ b/src/__tests__/commands/browser.test.ts @@ -8,6 +8,7 @@ import { handleBrowserExecute, handleBrowserList, handleBrowserClose, + handleBrowserQuickExecute, } from '../../commands/browser'; import { getClient } from '../../utils/client'; import { initializeConfig } from '../../utils/config'; @@ -129,4 +130,37 @@ describe('Browser Commands', () => { const { clearBrowserSession } = await import('../../utils/browser-session'); expect(clearBrowserSession).toHaveBeenCalled(); }); + + it('quick execute skips launch when session exists', async () => { + // loadBrowserSession already returns a stored session (from mock) + // so quick execute should NOT call browser() to launch + await handleBrowserQuickExecute({ code: 'open https://example.com' }); + + expect(mockClient.browser).not.toHaveBeenCalled(); + }); + + it('quick execute auto-launches when no session exists', async () => { + const { loadBrowserSession, saveBrowserSession } = + await import('../../utils/browser-session'); + vi.mocked(loadBrowserSession).mockReturnValueOnce(null); // no session first call + vi.mocked(loadBrowserSession).mockReturnValue({ + // session exists after launch + id: 'new-session', + cdpUrl: 'wss://new', + createdAt: '2025-01-01T00:00:00Z', + }); + + mockClient.browser.mockResolvedValue({ + success: true, + id: 'new-session', + cdpUrl: 'wss://new', + }); + + await handleBrowserQuickExecute({ code: 'open https://example.com' }); + + expect(mockClient.browser).toHaveBeenCalledTimes(1); + expect(saveBrowserSession).toHaveBeenCalledWith( + expect.objectContaining({ id: 'new-session' }) + ); + }); }); diff --git a/src/commands/browser.ts b/src/commands/browser.ts index 51dd098a64..4cbb76c439 100644 --- a/src/commands/browser.ts +++ b/src/commands/browser.ts @@ -49,6 +49,14 @@ export interface BrowserCloseOptions { json?: boolean; } +export interface BrowserQuickExecuteOptions { + code: string; + apiKey?: string; + apiUrl?: string; + output?: string; + json?: boolean; +} + /** * Launch a new browser session */ @@ -344,6 +352,59 @@ export async function handleBrowserList( } } +/** + * Shorthand: auto-launch a session if needed, then execute an agent-browser command. + * Enables `firecrawl browser "open example.com"` without a separate launch step. + */ +export async function handleBrowserQuickExecute( + options: BrowserQuickExecuteOptions +): Promise { + // Auto-launch if no active session + const existing = loadBrowserSession(); + if (!existing) { + try { + const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); + const data = await app.browser({}); + + if (!data.success) { + console.error('Error:', data.error || 'Failed to launch session'); + process.exit(1); + } + + saveBrowserSession({ + id: data.id!, + cdpUrl: data.cdpUrl!, + createdAt: new Date().toISOString(), + }); + + // Print session info to stderr so it doesn't mix with command output + console.error(`Session launched: ${data.id}`); + } catch (error) { + console.error( + 'Error:', + error instanceof Error ? error.message : 'Failed to launch session' + ); + process.exit(1); + } + } + + // Auto-prefix agent-browser if needed + let finalCode = options.code; + if (!finalCode.startsWith('agent-browser')) { + finalCode = `agent-browser ${finalCode}`; + } + + // Execute via the standard handler + await handleBrowserExecute({ + code: finalCode, + language: 'bash', + apiKey: options.apiKey, + apiUrl: options.apiUrl, + output: options.output, + json: options.json, + }); +} + /** * Close a browser session */ diff --git a/src/index.ts b/src/index.ts index 9a3c64cf48..77fb79d36f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import { handleBrowserExecute, handleBrowserList, handleBrowserClose, + handleBrowserQuickExecute, } from './commands/browser'; import { handleVersionCommand } from './commands/version'; import { handleLoginCommand } from './commands/login'; @@ -595,15 +596,37 @@ function createBrowserCommand(): Command { .description( 'Launch cloud browser sessions and execute Python, JavaScript, or bash code remotely via Playwright' ) + .argument('[code]', 'Shorthand: auto-launch session + execute command') + .option( + '-k, --api-key ', + 'Firecrawl API key (overrides global --api-key)' + ) + .option('--api-url ', 'API URL (overrides global --api-url)') + .option('-o, --output ', 'Output file path (default: stdout)') + .option('--json', 'Output as JSON format', false) + .action(async (code, options) => { + if (code) { + await handleBrowserQuickExecute({ + code, + apiKey: options.apiKey, + apiUrl: options.apiUrl, + output: options.output, + json: options.json, + }); + } + }) .addHelpText( 'after', ` -Examples: - $ firecrawl browser launch +Shorthand (auto-launches session if needed): + $ firecrawl browser "open https://example.com" + $ firecrawl browser "snapshot" + $ firecrawl browser "click @e5" + $ firecrawl browser "scrape" + +Explicit subcommands: + $ firecrawl browser launch-session $ firecrawl browser execute "open https://example.com" - $ firecrawl browser execute "snapshot" - $ firecrawl browser execute "click @e5" - $ firecrawl browser execute "scrape" $ firecrawl browser list active $ firecrawl browser close @@ -618,8 +641,10 @@ Examples: ); browserCmd - .command('launch') - .description('Launch a new cloud browser session') + .command('launch-session') + .description( + 'Launch a new cloud browser session (without executing a command)' + ) .option( '--ttl ', 'Total session TTL in seconds (default: 300)', @@ -641,11 +666,14 @@ Output: Prints the Session ID and CDP URL. The session is auto-saved so subsequent execute/close commands target it automatically. + Tip: Use the shorthand to launch + execute in one step: + $ firecrawl browser "open https://example.com" + Examples: - $ firecrawl browser launch - $ firecrawl browser launch --stream --ttl 600 - $ firecrawl browser launch --ttl 300 --ttl-inactivity 60 - $ firecrawl browser launch -o session.json --json + $ firecrawl browser launch-session + $ firecrawl browser launch-session --stream --ttl 600 + $ firecrawl browser launch-session --ttl 300 --ttl-inactivity 60 + $ firecrawl browser launch-session -o session.json --json ` ) .action(async (options) => { From 123fd6ed80e8c22e1ed1e2f2261e3ea8d5dde7e8 Mon Sep 17 00:00:00 2001 From: Nicolas <20311743+nickscamara@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:16:05 -0800 Subject: [PATCH 11/11] Nick: --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 1a27d533df..62700653be 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "vitest": "^4.0.0" }, "dependencies": { - "@mendable/firecrawl-js": "4.13.0-beta.3", + "@mendable/firecrawl-js": "4.13.0", "commander": "^14.0.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25a9dd195c..9395731996 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@mendable/firecrawl-js': - specifier: 4.13.0-beta.3 - version: 4.13.0-beta.3 + specifier: 4.13.0 + version: 4.13.0 commander: specifier: ^14.0.2 version: 14.0.2 @@ -195,8 +195,8 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@mendable/firecrawl-js@4.13.0-beta.3': - resolution: {integrity: sha512-OD8PmuQ1HdOanLub2xXVg4uCq1rJndlb/yuRYO1tUn697+ckgp0E/4yk3V49YoQM2ArW3/lIHCOKR/ifxp2ADg==} + '@mendable/firecrawl-js@4.13.0': + resolution: {integrity: sha512-8Y2+jjol4/ZXjxyf/5VUGCeOeGYWOOp03HZoyULWU5/adyUjnNL1DkI0/96wHs1aGL5/+JCq/hDfkXMUFBVYfQ==} engines: {node: '>=22.0.0'} '@rollup/rollup-android-arm-eabi@4.55.1': @@ -972,7 +972,7 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@mendable/firecrawl-js@4.13.0-beta.3': + '@mendable/firecrawl-js@4.13.0': dependencies: axios: 1.13.5 typescript-event-target: 1.1.2