From eb12c048d50f731edc8828472c978ac45bad3455 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:00:21 -0400 Subject: [PATCH 1/6] add firecrawl web defaults setup --- README.md | 13 ++ src/__tests__/commands/setup.test.ts | 17 ++ src/__tests__/utils/web-defaults.test.ts | 96 ++++++++++++ src/commands/init.ts | 3 + src/commands/setup.ts | 29 +++- src/index.ts | 11 +- src/utils/web-defaults.ts | 192 +++++++++++++++++++++++ 7 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/utils/web-defaults.test.ts create mode 100644 src/utils/web-defaults.ts diff --git a/README.md b/README.md index 3d72d06b83..e2a0746505 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,19 @@ To install the Firecrawl MCP server into your editors (Cursor, Claude Code, VS C firecrawl setup mcp ``` +To make Firecrawl the default web provider for supported AI agents: + +```bash +firecrawl setup defaults +``` + +This disables native web fetch/search where supported so agents route web work +through Firecrawl. To undo those config changes: + +```bash +firecrawl setup defaults --undo +``` + ## Quick Start Just run a command - the CLI will prompt you to authenticate if needed: diff --git a/src/__tests__/commands/setup.test.ts b/src/__tests__/commands/setup.test.ts index 8485d1fd45..2c30d874b6 100644 --- a/src/__tests__/commands/setup.test.ts +++ b/src/__tests__/commands/setup.test.ts @@ -1,11 +1,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { execSync } from 'child_process'; import { handleSetupCommand } from '../../commands/setup'; +import { configureWebDefaults } from '../../utils/web-defaults'; vi.mock('child_process', () => ({ execSync: vi.fn(), })); +vi.mock('../../utils/web-defaults', () => ({ + configureWebDefaults: vi.fn(async () => []), +})); + describe('handleSetupCommand', () => { beforeEach(() => { vi.clearAllMocks(); @@ -50,6 +55,18 @@ describe('handleSetupCommand', () => { ); }); + it('configures Firecrawl as the default web provider', async () => { + await handleSetupCommand('defaults', {}); + + expect(configureWebDefaults).toHaveBeenCalledWith({ undo: undefined }); + }); + + it('undoes default web provider config', async () => { + await handleSetupCommand('defaults', { undo: true }); + + expect(configureWebDefaults).toHaveBeenCalledWith({ undo: true }); + }); + it('strips inherited npm_* env vars before nested npx calls', async () => { // Reproduces the bug where running this CLI under `npx -y firecrawl-cli@VERSION` // leaks npm_command/npm_lifecycle_event/npm_execpath into nested diff --git a/src/__tests__/utils/web-defaults.test.ts b/src/__tests__/utils/web-defaults.test.ts new file mode 100644 index 0000000000..af3d072a85 --- /dev/null +++ b/src/__tests__/utils/web-defaults.test.ts @@ -0,0 +1,96 @@ +import { promises as fs } from 'fs'; +import os from 'os'; +import path from 'path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { configureWebDefaults } from '../../utils/web-defaults'; + +const originalHome = process.env.HOME; +let tempHome: string; + +async function read(relativePath: string): Promise { + return fs.readFile(path.join(tempHome, relativePath), 'utf8'); +} + +async function write(relativePath: string, content: string): Promise { + const filePath = path.join(tempHome, relativePath); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, content, 'utf8'); +} + +describe('configureWebDefaults', () => { + beforeEach(async () => { + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), 'firecrawl-web-')); + process.env.HOME = tempHome; + }); + + afterEach(async () => { + if (originalHome === undefined) delete process.env.HOME; + else process.env.HOME = originalHome; + await fs.rm(tempHome, { recursive: true, force: true }); + }); + + it('disables native Claude Code and Codex web tools', async () => { + const results = await configureWebDefaults(); + + expect(results.map((result) => result.changed)).toEqual([true, true]); + expect(JSON.parse(await read('.claude/settings.json'))).toEqual({ + permissions: { + deny: ['WebSearch', 'WebFetch'], + }, + }); + expect(await read('.codex/config.toml')).toBe('web_search = "disabled"\n'); + }); + + it('preserves existing Claude permissions and Codex config while disabling web', async () => { + await write( + '.claude/settings.json', + JSON.stringify({ + permissions: { + allow: ['Read'], + deny: ['Bash(rm *)', 'WebSearch'], + }, + }) + ); + await write( + '.codex/config.toml', + 'model = "gpt-5"\nweb_search = "cached"\n' + ); + + await configureWebDefaults(); + + expect(JSON.parse(await read('.claude/settings.json'))).toEqual({ + permissions: { + allow: ['Read'], + deny: ['Bash(rm *)', 'WebSearch', 'WebFetch'], + }, + }); + expect(await read('.codex/config.toml')).toBe( + 'model = "gpt-5"\nweb_search = "disabled"\n' + ); + }); + + it('undoes only the native web defaults', async () => { + await write( + '.claude/settings.json', + JSON.stringify({ + permissions: { + deny: ['Bash(rm *)', 'WebSearch', 'WebFetch'], + }, + }) + ); + await write( + '.codex/config.toml', + 'model = "gpt-5"\nweb_search = "disabled"\n' + ); + + const results = await configureWebDefaults({ undo: true }); + + expect(results.map((result) => result.changed)).toEqual([true, true]); + expect(JSON.parse(await read('.claude/settings.json'))).toEqual({ + permissions: { + deny: ['Bash(rm *)'], + }, + }); + expect(await read('.codex/config.toml')).toBe('model = "gpt-5"\n'); + }); +}); diff --git a/src/commands/init.ts b/src/commands/init.ts index 1bac7fc6e4..f3f55bc159 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -219,6 +219,9 @@ function printNextSteps(skillCount: number | null): void { console.log( ` ${arrow} ${dim}Add MCP: ${reset} ${bold}firecrawl setup mcp${reset}` ); + console.log( + ` ${arrow} ${dim}Default web:${reset} ${bold}firecrawl setup defaults${reset}` + ); console.log( ` ${arrow} ${dim}All commands:${reset} ${bold}firecrawl --help${reset}` ); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 8176aae18f..fff4a49dc5 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -12,12 +12,14 @@ import { WORKFLOW_SKILL_REPOS, } from './skills-install'; import { hasNpx, installSkillsNative } from './skills-native'; +import { configureWebDefaults } from '../utils/web-defaults'; -export type SetupSubcommand = 'skills' | 'workflows' | 'mcp'; +export type SetupSubcommand = 'skills' | 'workflows' | 'mcp' | 'defaults'; export interface SetupOptions { global?: boolean; agent?: string; + undo?: boolean; } /** @@ -37,6 +39,9 @@ export async function handleSetupCommand( case 'mcp': await installMcp(options); break; + case 'defaults': + await setupDefaults(options); + break; default: console.error(`Unknown setup subcommand: ${subcommand}`); console.log('\nAvailable subcommands:'); @@ -49,10 +54,32 @@ export async function handleSetupCommand( console.log( ' mcp Install firecrawl MCP server into editors (Cursor, Claude Code, VS Code, etc.)' ); + console.log( + ' defaults Make Firecrawl the default web provider for supported AI agents' + ); process.exit(1); } } +async function setupDefaults(options: SetupOptions): Promise { + const results = await configureWebDefaults({ undo: options.undo }); + + for (const result of results) { + const prefix = result.skipped ? '!' : result.changed ? '✓' : '•'; + console.log(`${prefix} ${result.message}`); + console.log(` ${result.path}`); + } + + console.log(''); + if (options.undo) { + console.log('Native web tools restored where supported.'); + } else { + console.log( + 'Firecrawl is now the default web provider for supported AI agents.' + ); + } +} + async function installSkills( options: SetupOptions, repos: readonly string[] diff --git a/src/index.ts b/src/index.ts index 2b63ddf317..8e3ed881df 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1809,11 +1809,18 @@ program program .command('setup') .description( - 'Set up individual firecrawl integrations (skills, workflows, mcp)' + 'Set up individual firecrawl integrations (skills, workflows, mcp, defaults)' + ) + .argument( + '', + 'What to set up: "skills", "workflows", "mcp", or "defaults"' ) - .argument('', 'What to set up: "skills", "workflows", or "mcp"') .option('-g, --global', 'Install globally (user-level)') .option('-a, --agent ', 'Install to a specific agent') + .option( + '--undo', + 'Undo setup defaults by re-enabling native web tools where supported' + ) .action(async (subcommand: SetupSubcommand, options) => { await handleSetupCommand(subcommand, options); }); diff --git a/src/utils/web-defaults.ts b/src/utils/web-defaults.ts new file mode 100644 index 0000000000..0949634096 --- /dev/null +++ b/src/utils/web-defaults.ts @@ -0,0 +1,192 @@ +import { promises as fs } from 'fs'; +import os from 'os'; +import path from 'path'; + +const CLAUDE_DENY_TOOLS = ['WebSearch', 'WebFetch'] as const; +const CODEX_WEB_SEARCH_DISABLED = 'web_search = "disabled"'; + +export interface WebDefaultsOptions { + undo?: boolean; +} + +export interface WebDefaultResult { + agent: 'Claude Code' | 'Codex'; + path: string; + changed: boolean; + skipped?: boolean; + message: string; +} + +async function readText(filePath: string): Promise { + try { + return await fs.readFile(filePath, 'utf8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw error; + } +} + +async function writeText(filePath: string, content: string): Promise { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, content, 'utf8'); +} + +function removeJsonComments(content: string): string { + return content + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/(^|[^:\\])\/\/.*$/gm, '$1'); +} + +async function configureClaudeDefaults( + undo: boolean +): Promise { + const filePath = path.join(os.homedir(), '.claude', 'settings.json'); + const existing = await readText(filePath); + let config: Record = {}; + + if (existing && existing.trim()) { + try { + config = JSON.parse(removeJsonComments(existing)); + } catch { + return { + agent: 'Claude Code', + path: filePath, + changed: false, + skipped: true, + message: + 'Skipped Claude Code settings because settings.json is not valid JSON', + }; + } + } + + const permissions = + config.permissions && typeof config.permissions === 'object' + ? (config.permissions as Record) + : {}; + const deny = Array.isArray(permissions.deny) + ? permissions.deny.filter( + (value): value is string => typeof value === 'string' + ) + : []; + + let nextDeny: string[]; + const denyTools = new Set(CLAUDE_DENY_TOOLS); + if (undo) { + nextDeny = deny.filter((tool) => !denyTools.has(tool)); + } else { + nextDeny = Array.from(new Set([...deny, ...CLAUDE_DENY_TOOLS])); + } + + const changed = + deny.length !== nextDeny.length || + deny.some((tool, index) => tool !== nextDeny[index]); + + if (!changed) { + return { + agent: 'Claude Code', + path: filePath, + changed: false, + message: undo + ? 'Claude Code native WebSearch/WebFetch were already enabled' + : 'Claude Code already denies native WebSearch/WebFetch', + }; + } + + const nextPermissions = { ...permissions }; + if (nextDeny.length > 0) { + nextPermissions.deny = nextDeny; + } else { + delete nextPermissions.deny; + } + + const nextConfig = { ...config }; + if (Object.keys(nextPermissions).length > 0) { + nextConfig.permissions = nextPermissions; + } else { + delete nextConfig.permissions; + } + + await writeText(filePath, `${JSON.stringify(nextConfig, null, 2)}\n`); + + return { + agent: 'Claude Code', + path: filePath, + changed: true, + message: undo + ? 'Enabled Claude Code native WebSearch/WebFetch' + : 'Disabled Claude Code native WebSearch/WebFetch', + }; +} + +function setCodexWebSearchDisabled(content: string): { + content: string; + changed: boolean; +} { + const webSearchLine = /^web_search\s*=\s*("[^"]*"|'[^']*'|[^\r\n#]+).*$/m; + + if (webSearchLine.test(content)) { + const next = content.replace(webSearchLine, CODEX_WEB_SEARCH_DISABLED); + return { content: next, changed: next !== content }; + } + + const prefix = + content.trim().length > 0 && !content.endsWith('\n') ? '\n' : ''; + return { + content: `${content}${prefix}${CODEX_WEB_SEARCH_DISABLED}\n`, + changed: true, + }; +} + +function removeCodexWebSearchDisabled(content: string): { + content: string; + changed: boolean; +} { + const lines = content.split(/\r?\n/); + const nextLines = lines.filter( + (line) => !/^web_search\s*=\s*["']disabled["']\s*(#.*)?$/.test(line.trim()) + ); + const next = nextLines.join('\n').replace(/\n{3,}/g, '\n\n'); + return { content: next, changed: next !== content }; +} + +async function configureCodexDefaults( + undo: boolean +): Promise { + const filePath = path.join(os.homedir(), '.codex', 'config.toml'); + const existing = (await readText(filePath)) ?? ''; + const result = undo + ? removeCodexWebSearchDisabled(existing) + : setCodexWebSearchDisabled(existing); + + if (!result.changed) { + return { + agent: 'Codex', + path: filePath, + changed: false, + message: undo + ? 'Codex native web search was already enabled' + : 'Codex native web search was already disabled', + }; + } + + await writeText(filePath, result.content); + + return { + agent: 'Codex', + path: filePath, + changed: true, + message: undo + ? 'Enabled Codex native web search' + : 'Disabled Codex native web search', + }; +} + +export async function configureWebDefaults( + options: WebDefaultsOptions = {} +): Promise { + const undo = Boolean(options.undo); + return Promise.all([ + configureClaudeDefaults(undo), + configureCodexDefaults(undo), + ]); +} From 875f721dd6a146713add0c63f2e91476b97fa3ad Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:09:57 -0400 Subject: [PATCH 2/6] harden web defaults config edits --- src/__tests__/utils/web-defaults.test.ts | 26 ++++++++++ src/utils/web-defaults.ts | 60 ++++++++++++++---------- 2 files changed, 62 insertions(+), 24 deletions(-) diff --git a/src/__tests__/utils/web-defaults.test.ts b/src/__tests__/utils/web-defaults.test.ts index af3d072a85..b2c2670249 100644 --- a/src/__tests__/utils/web-defaults.test.ts +++ b/src/__tests__/utils/web-defaults.test.ts @@ -93,4 +93,30 @@ describe('configureWebDefaults', () => { }); expect(await read('.codex/config.toml')).toBe('model = "gpt-5"\n'); }); + + it('writes Codex web_search at the root before TOML tables', async () => { + await write( + '.codex/config.toml', + 'model = "gpt-5"\n\n[mcp_servers.firecrawl]\ncommand = "npx"\n' + ); + + await configureWebDefaults(); + + expect(await read('.codex/config.toml')).toBe( + 'model = "gpt-5"\n\nweb_search = "disabled"\n[mcp_servers.firecrawl]\ncommand = "npx"\n' + ); + }); + + it('does not undo table-local Codex web_search settings', async () => { + await write( + '.codex/config.toml', + 'model = "gpt-5"\n\n[profiles.research]\nweb_search = "disabled"\n' + ); + + await configureWebDefaults({ undo: true }); + + expect(await read('.codex/config.toml')).toBe( + 'model = "gpt-5"\n\n[profiles.research]\nweb_search = "disabled"\n' + ); + }); }); diff --git a/src/utils/web-defaults.ts b/src/utils/web-defaults.ts index 0949634096..4adddea30e 100644 --- a/src/utils/web-defaults.ts +++ b/src/utils/web-defaults.ts @@ -63,23 +63,25 @@ async function configureClaudeDefaults( config.permissions && typeof config.permissions === 'object' ? (config.permissions as Record) : {}; - const deny = Array.isArray(permissions.deny) - ? permissions.deny.filter( - (value): value is string => typeof value === 'string' - ) - : []; + const deny = Array.isArray(permissions.deny) ? [...permissions.deny] : []; - let nextDeny: string[]; + let nextDeny: unknown[]; const denyTools = new Set(CLAUDE_DENY_TOOLS); if (undo) { - nextDeny = deny.filter((tool) => !denyTools.has(tool)); + nextDeny = deny.filter( + (tool) => typeof tool !== 'string' || !denyTools.has(tool) + ); } else { - nextDeny = Array.from(new Set([...deny, ...CLAUDE_DENY_TOOLS])); + const existing = new Set( + deny.filter((tool): tool is string => typeof tool === 'string') + ); + nextDeny = [...deny]; + for (const tool of CLAUDE_DENY_TOOLS) { + if (!existing.has(tool)) nextDeny.push(tool); + } } - const changed = - deny.length !== nextDeny.length || - deny.some((tool, index) => tool !== nextDeny[index]); + const changed = JSON.stringify(deny) !== JSON.stringify(nextDeny); if (!changed) { return { @@ -122,19 +124,26 @@ function setCodexWebSearchDisabled(content: string): { content: string; changed: boolean; } { - const webSearchLine = /^web_search\s*=\s*("[^"]*"|'[^']*'|[^\r\n#]+).*$/m; + if (content.trim().length === 0) { + return { content: `${CODEX_WEB_SEARCH_DISABLED}\n`, changed: true }; + } - if (webSearchLine.test(content)) { - const next = content.replace(webSearchLine, CODEX_WEB_SEARCH_DISABLED); - return { content: next, changed: next !== content }; + const lines = content.split(/\r?\n/); + const firstTableIndex = lines.findIndex((line) => /^\s*\[/.test(line)); + const rootEnd = firstTableIndex === -1 ? lines.length : firstTableIndex; + + for (let index = 0; index < rootEnd; index += 1) { + if (/^\s*web_search\s*=/.test(lines[index])) { + if (lines[index] === CODEX_WEB_SEARCH_DISABLED) { + return { content, changed: false }; + } + lines[index] = CODEX_WEB_SEARCH_DISABLED; + return { content: lines.join('\n'), changed: true }; + } } - const prefix = - content.trim().length > 0 && !content.endsWith('\n') ? '\n' : ''; - return { - content: `${content}${prefix}${CODEX_WEB_SEARCH_DISABLED}\n`, - changed: true, - }; + lines.splice(rootEnd, 0, CODEX_WEB_SEARCH_DISABLED); + return { content: lines.join('\n'), changed: true }; } function removeCodexWebSearchDisabled(content: string): { @@ -142,9 +151,12 @@ function removeCodexWebSearchDisabled(content: string): { changed: boolean; } { const lines = content.split(/\r?\n/); - const nextLines = lines.filter( - (line) => !/^web_search\s*=\s*["']disabled["']\s*(#.*)?$/.test(line.trim()) - ); + const firstTableIndex = lines.findIndex((line) => /^\s*\[/.test(line)); + const rootEnd = firstTableIndex === -1 ? lines.length : firstTableIndex; + const nextLines = lines.filter((line, index) => { + if (index >= rootEnd) return true; + return !/^web_search\s*=\s*["']disabled["']\s*(#.*)?$/.test(line.trim()); + }); const next = nextLines.join('\n').replace(/\n{3,}/g, '\n\n'); return { content: next, changed: next !== content }; } From 8e5ac9a55c158e2dda457d25ef9dfb85b9116c9d Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Fri, 5 Jun 2026 18:25:23 -0400 Subject: [PATCH 3/6] Update setup.ts --- src/commands/setup.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/commands/setup.ts b/src/commands/setup.ts index fff4a49dc5..b312bdf9b1 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -39,9 +39,6 @@ export async function handleSetupCommand( case 'mcp': await installMcp(options); break; - case 'defaults': - await setupDefaults(options); - break; default: console.error(`Unknown setup subcommand: ${subcommand}`); console.log('\nAvailable subcommands:'); @@ -54,14 +51,13 @@ export async function handleSetupCommand( console.log( ' mcp Install firecrawl MCP server into editors (Cursor, Claude Code, VS Code, etc.)' ); - console.log( - ' defaults Make Firecrawl the default web provider for supported AI agents' - ); process.exit(1); } } -async function setupDefaults(options: SetupOptions): Promise { +export async function handleMakeDefaultCommand( + options: SetupOptions = {} +): Promise { const results = await configureWebDefaults({ undo: options.undo }); for (const result of results) { From 4af64c8ea9939e908a5b0ea62437a3fc9dccfa42 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:37:40 -0400 Subject: [PATCH 4/6] bump version to 1.19.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1cf60a6ec1..aa1971d62b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firecrawl-cli", - "version": "1.19.0", + "version": "1.19.1", "description": "Command-line interface for Firecrawl. Scrape, crawl, and extract data from any website directly from your terminal.", "main": "dist/index.js", "bin": { From b6983a0642ad15f69b328552c2309682a90c0b20 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:39:08 -0400 Subject: [PATCH 5/6] update pinned version to 1.19.1 in docs --- README.md | 4 ++-- skills/firecrawl-cli/rules/install.md | 8 ++++---- skills/firecrawl-cli/rules/security.md | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e2a0746505..ff8f49a8e9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ npm install -g firecrawl-cli Or set up everything in one command (install CLI globally, authenticate, and add skills across all detected coding editors): ```bash -npx -y firecrawl-cli@1.16.2 init -y --browser +npx -y firecrawl-cli@1.19.1 init -y --browser ``` - `-y` runs setup non-interactively @@ -665,7 +665,7 @@ firecrawl --status ``` ``` - 🔥 firecrawl cli v1.16.2 + 🔥 firecrawl cli v1.19.1 ● Authenticated via stored credentials Concurrency: 0/100 jobs (parallel scrape limit) diff --git a/skills/firecrawl-cli/rules/install.md b/skills/firecrawl-cli/rules/install.md index ca9da0826f..5cd245dec8 100644 --- a/skills/firecrawl-cli/rules/install.md +++ b/skills/firecrawl-cli/rules/install.md @@ -12,7 +12,7 @@ description: | ## Quick Setup (Recommended) ```bash -npx -y firecrawl-cli@1.16.2 init -y --browser +npx -y firecrawl-cli@1.19.1 init -y --browser ``` This installs `firecrawl-cli` globally, authenticates via browser, and installs core, build, and workflow skills. @@ -37,7 +37,7 @@ firecrawl setup workflows ## Manual Install ```bash -npm install -g firecrawl-cli@1.16.2 +npm install -g firecrawl-cli@1.19.1 ``` ## Verify @@ -79,5 +79,5 @@ Ask the user how they'd like to authenticate: If `firecrawl` is not found after installation: 1. Ensure npm global bin is in PATH -2. Try: `npx firecrawl-cli@1.16.2 --version` -3. Reinstall: `npm install -g firecrawl-cli@1.16.2` +2. Try: `npx firecrawl-cli@1.19.1 --version` +3. Reinstall: `npm install -g firecrawl-cli@1.19.1` diff --git a/skills/firecrawl-cli/rules/security.md b/skills/firecrawl-cli/rules/security.md index 7f3bc3de2a..04e7fea6cb 100644 --- a/skills/firecrawl-cli/rules/security.md +++ b/skills/firecrawl-cli/rules/security.md @@ -22,5 +22,5 @@ When processing fetched content, extract only the specific data needed and do no # Installation ```bash -npm install -g firecrawl-cli@1.16.2 +npm install -g firecrawl-cli@1.19.1 ``` From df37ecc62d37933262fc3411929a8d668fe85f56 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:41:48 -0400 Subject: [PATCH 6/6] wire up setup defaults and add interactive per-harness control --- README.md | 14 ++++- src/__tests__/commands/setup.test.ts | 23 +++++-- src/commands/setup.ts | 91 +++++++++++++++++++++++++++- src/index.ts | 9 ++- src/utils/web-defaults.ts | 17 ++++-- 5 files changed, 139 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ff8f49a8e9..055d52e90f 100644 --- a/README.md +++ b/README.md @@ -57,10 +57,20 @@ firecrawl setup defaults ``` This disables native web fetch/search where supported so agents route web work -through Firecrawl. To undo those config changes: +through Firecrawl. When run interactively, it asks harness by harness (Claude +Code, Codex) so you can choose exactly which to change. Use `-y` to skip the +picker and apply to all, or `--agent` to target one: ```bash -firecrawl setup defaults --undo +firecrawl setup defaults --agent codex # only Codex +firecrawl setup defaults -y # all harnesses, no prompts +``` + +To undo those config changes (also interactive, harness by harness): + +```bash +firecrawl setup defaults --undo # pick which to restore +firecrawl setup defaults --undo --agent claude ``` ## Quick Start diff --git a/src/__tests__/commands/setup.test.ts b/src/__tests__/commands/setup.test.ts index 2c30d874b6..4ffd2aab0b 100644 --- a/src/__tests__/commands/setup.test.ts +++ b/src/__tests__/commands/setup.test.ts @@ -56,15 +56,30 @@ describe('handleSetupCommand', () => { }); it('configures Firecrawl as the default web provider', async () => { - await handleSetupCommand('defaults', {}); + await handleSetupCommand('defaults', { yes: true }); - expect(configureWebDefaults).toHaveBeenCalledWith({ undo: undefined }); + expect(configureWebDefaults).toHaveBeenCalledWith({ + undo: false, + agents: undefined, + }); }); it('undoes default web provider config', async () => { - await handleSetupCommand('defaults', { undo: true }); + await handleSetupCommand('defaults', { undo: true, yes: true }); - expect(configureWebDefaults).toHaveBeenCalledWith({ undo: true }); + expect(configureWebDefaults).toHaveBeenCalledWith({ + undo: true, + agents: undefined, + }); + }); + + it('limits defaults config to a single agent', async () => { + await handleSetupCommand('defaults', { undo: true, agent: 'codex' }); + + expect(configureWebDefaults).toHaveBeenCalledWith({ + undo: true, + agents: ['Codex'], + }); }); it('strips inherited npm_* env vars before nested npx calls', async () => { diff --git a/src/commands/setup.ts b/src/commands/setup.ts index b312bdf9b1..06fc2e7c46 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -4,6 +4,7 @@ */ import { execSync } from 'child_process'; +import readline from 'readline'; import { getApiKey } from '../utils/config'; import { buildSkillsInstallArgs, @@ -12,7 +13,11 @@ import { WORKFLOW_SKILL_REPOS, } from './skills-install'; import { hasNpx, installSkillsNative } from './skills-native'; -import { configureWebDefaults } from '../utils/web-defaults'; +import { + configureWebDefaults, + WEB_AGENTS, + type WebAgent, +} from '../utils/web-defaults'; export type SetupSubcommand = 'skills' | 'workflows' | 'mcp' | 'defaults'; @@ -20,6 +25,8 @@ export interface SetupOptions { global?: boolean; agent?: string; undo?: boolean; + /** Skip the interactive harness picker and apply to all agents. */ + yes?: boolean; } /** @@ -39,6 +46,9 @@ export async function handleSetupCommand( case 'mcp': await installMcp(options); break; + case 'defaults': + await handleMakeDefaultCommand(options); + break; default: console.error(`Unknown setup subcommand: ${subcommand}`); console.log('\nAvailable subcommands:'); @@ -51,14 +61,89 @@ export async function handleSetupCommand( console.log( ' mcp Install firecrawl MCP server into editors (Cursor, Claude Code, VS Code, etc.)' ); + console.log( + ' defaults Make Firecrawl the default web provider (use --undo to restore native web tools)' + ); process.exit(1); } } +/** Map a user-supplied --agent value to a known web agent. */ +function resolveWebAgent(agent: string): WebAgent | null { + const normalized = agent.trim().toLowerCase(); + if (normalized === 'claude' || normalized === 'claude code') { + return 'Claude Code'; + } + if (normalized === 'codex') return 'Codex'; + return null; +} + +function promptInput(question: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + return new Promise((resolve) => { + rl.question(question, (answer: string) => { + rl.close(); + resolve(answer.trim()); + }); + }); +} + +/** + * Interactively ask which harnesses to apply the change to, one by one. + * Returns the selected agents, or null if the user aborted. + */ +async function pickWebAgents(undo: boolean): Promise { + const verb = undo + ? 'Re-enable native web tools for' + : 'Disable native web tools for'; + console.log( + undo + ? 'Choose which harnesses to restore native web tools for:' + : 'Choose which harnesses to route through Firecrawl:' + ); + console.log(''); + + const selected: WebAgent[] = []; + for (const agent of WEB_AGENTS) { + const answer = ( + await promptInput(` ${verb} ${agent}? [Y/n] `) + ).toLowerCase(); + if (answer === '' || answer === 'y' || answer === 'yes') { + selected.push(agent); + } + } + console.log(''); + return selected; +} + export async function handleMakeDefaultCommand( options: SetupOptions = {} ): Promise { - const results = await configureWebDefaults({ undo: options.undo }); + const undo = Boolean(options.undo); + let agents: readonly WebAgent[] | undefined; + + if (options.agent) { + const resolved = resolveWebAgent(options.agent); + if (!resolved) { + console.error( + `Unknown agent "${options.agent}" for setup defaults. Use "claude" or "codex".` + ); + process.exit(1); + } + agents = [resolved]; + } else if (!options.yes && process.stdin.isTTY) { + const picked = await pickWebAgents(undo); + if (!picked || picked.length === 0) { + console.log('No harnesses selected. Nothing changed.'); + return; + } + agents = picked; + } + + const results = await configureWebDefaults({ undo, agents }); for (const result of results) { const prefix = result.skipped ? '!' : result.changed ? '✓' : '•'; @@ -67,7 +152,7 @@ export async function handleMakeDefaultCommand( } console.log(''); - if (options.undo) { + if (undo) { console.log('Native web tools restored where supported.'); } else { console.log( diff --git a/src/index.ts b/src/index.ts index 8e3ed881df..37b8e364dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1816,7 +1816,14 @@ program 'What to set up: "skills", "workflows", "mcp", or "defaults"' ) .option('-g, --global', 'Install globally (user-level)') - .option('-a, --agent ', 'Install to a specific agent') + .option( + '-a, --agent ', + 'Limit to a specific agent (for "defaults": "claude" or "codex")' + ) + .option( + '-y, --yes', + 'For "defaults": skip the interactive harness picker and apply to all' + ) .option( '--undo', 'Undo setup defaults by re-enabling native web tools where supported' diff --git a/src/utils/web-defaults.ts b/src/utils/web-defaults.ts index 4adddea30e..87734ed2d8 100644 --- a/src/utils/web-defaults.ts +++ b/src/utils/web-defaults.ts @@ -5,12 +5,18 @@ import path from 'path'; const CLAUDE_DENY_TOOLS = ['WebSearch', 'WebFetch'] as const; const CODEX_WEB_SEARCH_DISABLED = 'web_search = "disabled"'; +export type WebAgent = 'Claude Code' | 'Codex'; + +export const WEB_AGENTS: readonly WebAgent[] = ['Claude Code', 'Codex']; + export interface WebDefaultsOptions { undo?: boolean; + /** Limit configuration to these agents. Defaults to all agents. */ + agents?: readonly WebAgent[]; } export interface WebDefaultResult { - agent: 'Claude Code' | 'Codex'; + agent: WebAgent; path: string; changed: boolean; skipped?: boolean; @@ -197,8 +203,9 @@ export async function configureWebDefaults( options: WebDefaultsOptions = {} ): Promise { const undo = Boolean(options.undo); - return Promise.all([ - configureClaudeDefaults(undo), - configureCodexDefaults(undo), - ]); + const selected = new Set(options.agents ?? WEB_AGENTS); + const tasks: Promise[] = []; + if (selected.has('Claude Code')) tasks.push(configureClaudeDefaults(undo)); + if (selected.has('Codex')) tasks.push(configureCodexDefaults(undo)); + return Promise.all(tasks); }