From 4a9b63955e140774af6b7b3434e4c0755c95aedc Mon Sep 17 00:00:00 2001 From: Kris Oei <87241105+KrisOei@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:38:17 -0700 Subject: [PATCH 1/9] feat(index): add a search command --- README.md | 31 ++- src/__tests__/cli-argv.test.ts | 34 +++ .../commands/legal-regulatory.test.ts | 200 ++++++++++++++++++ src/commands/legal-regulatory.ts | 78 +++++++ src/commands/list.ts | 3 +- src/index.ts | 48 +++++ src/types/legal-regulatory.ts | 21 ++ 7 files changed, 413 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/commands/legal-regulatory.test.ts create mode 100644 src/commands/legal-regulatory.ts create mode 100644 src/types/legal-regulatory.ts diff --git a/README.md b/README.md index 7536ef7c69..42e599a78d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🔥 Firecrawl CLI -Command-line interface for Firecrawl. Search, scrape, interact, crawl, map, search research papers and developer sources, and run agent jobs directly from your terminal. +Command-line interface for Firecrawl. Search, scrape, interact, crawl, map, search research papers, developer sources, and legal and regulatory sources, and run agent jobs directly from your terminal. ## Installation @@ -409,6 +409,35 @@ firecrawl developer "tokio select cancellation safety" --json -o results.json --- +### `legal-regulatory` - Search legal and regulatory sources + +Search the Legal and Regulatory Index: primary law and regulatory material from US federal, state, and local government sources, including statutes, regulations, codes, court opinions, and other government publications. + +```bash +firecrawl legal-regulatory "food labeling requirements for allergens" +``` + +#### Options + +| Option | Description | +| --------------------- | ----------------------------------------- | +| `--limit ` | Number of results (default: 10, max: 100) | +| `-o, --output ` | Save to file | +| `--json` | Output the raw response as JSON | +| `--pretty` | Pretty print JSON output | + +#### Examples + +```bash +# Find state statutes on a topic +firecrawl legal-regulatory "California data breach notification statute" --limit 10 + +# Keep the raw response +firecrawl legal-regulatory "FDA food labeling regulations" --json -o results.json +``` + +--- + ### `research` - Search research papers Search Firecrawl's research paper index: roughly 43M abstracts, around 90% biomedical (PubMed, bioRxiv, medRxiv) plus arXiv. Use this for biomedical, clinical, and scientific literature rather than scraping PubMed, bioRxiv, or Google Scholar by hand. diff --git a/src/__tests__/cli-argv.test.ts b/src/__tests__/cli-argv.test.ts index 139c926bd1..f501553d7a 100644 --- a/src/__tests__/cli-argv.test.ts +++ b/src/__tests__/cli-argv.test.ts @@ -88,6 +88,40 @@ describe('CLI argv parsing', () => { expect(result.stderr).not.toContain('unknown command'); }); + testWithBuiltCli( + 'lists the legal-regulatory command in root help output', + () => { + const result = spawnSync(process.execPath, [cliPath, '--help'], { + cwd: process.cwd(), + encoding: 'utf8', + }); + + expect(result.status).toBe(0); + expect(result.stdout).toMatch(/^\s*legal-regulatory\b/m); + } + ); + + testWithBuiltCli( + 'parses the legal-regulatory command and shows its help', + () => { + const result = spawnSync( + process.execPath, + [cliPath, 'legal-regulatory', '--help'], + { + cwd: process.cwd(), + encoding: 'utf8', + } + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('Usage: firecrawl legal-regulatory'); + expect(result.stdout).toContain('--limit'); + expect(result.stdout).toContain('--json'); + expect(result.stdout).toContain('--output'); + expect(result.stderr).not.toContain('unknown command'); + } + ); + testWithBuiltCli( 'describes default search highlights and public developer coverage', () => { diff --git a/src/__tests__/commands/legal-regulatory.test.ts b/src/__tests__/commands/legal-regulatory.test.ts new file mode 100644 index 0000000000..21b2ef3f8c --- /dev/null +++ b/src/__tests__/commands/legal-regulatory.test.ts @@ -0,0 +1,200 @@ +/** + * Tests for legal-regulatory command + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { handleLegalRegulatorySearchCommand } from '../../commands/legal-regulatory'; +import { getClient } from '../../utils/client'; +import { initializeConfig } from '../../utils/config'; +import { writeOutput } from '../../utils/output'; +import { setupTest, teardownTest } from '../utils/mock-client'; + +vi.mock('../../utils/output', () => ({ writeOutput: vi.fn() })); + +vi.mock('../../utils/client', async () => { + const actual = await vi.importActual('../../utils/client'); + return { + ...actual, + getClient: vi.fn(), + }; +}); + +describe('handleLegalRegulatorySearchCommand', () => { + let mockHttpGet: ReturnType; + + // Wrap a payload in the axios envelope returned by `client.http.get`. + const mockLegalRegulatoryResponse = (web: any[]) => ({ + data: { success: true, data: { web } }, + }); + + const sampleResult = { + url: 'https://www.ecfr.gov/current/title-21/chapter-I/subchapter-B/part-101', + title: '21 CFR Part 101 -- Food Labeling', + description: 'Food labeling requirements for packaged foods.', + position: 1, + }; + + beforeEach(() => { + setupTest(); + initializeConfig({ + apiKey: 'test-api-key', + apiUrl: 'https://api.firecrawl.dev', + }); + + mockHttpGet = vi.fn(); + vi.mocked(getClient).mockReturnValue({ + http: { get: mockHttpGet }, + } as any); + }); + + afterEach(() => { + teardownTest(); + vi.clearAllMocks(); + }); + + describe('API call generation', () => { + it('calls /v2/search/legal-regulatory with the query', async () => { + mockHttpGet.mockResolvedValue( + mockLegalRegulatoryResponse([sampleResult]) + ); + + await handleLegalRegulatorySearchCommand({ query: 'food labeling' }); + + expect(mockHttpGet).toHaveBeenCalledTimes(1); + expect(mockHttpGet).toHaveBeenCalledWith( + '/v2/search/legal-regulatory?query=food+labeling&integration=cli' + ); + }); + + it('passes k when a result count is provided', async () => { + mockHttpGet.mockResolvedValue( + mockLegalRegulatoryResponse([sampleResult]) + ); + + await handleLegalRegulatorySearchCommand({ + query: 'food labeling', + k: 5, + }); + + expect(mockHttpGet).toHaveBeenCalledWith( + '/v2/search/legal-regulatory?query=food+labeling&k=5&integration=cli' + ); + }); + + it('passes apiUrl and apiKey to getClient when provided', async () => { + mockHttpGet.mockResolvedValue(mockLegalRegulatoryResponse([])); + + await handleLegalRegulatorySearchCommand({ + query: 'test', + apiKey: 'other-key', + apiUrl: 'http://localhost:3002', + }); + + expect(getClient).toHaveBeenCalledWith({ + apiKey: 'other-key', + apiUrl: 'http://localhost:3002', + }); + }); + }); + + describe('output', () => { + it('renders numbered title, url, and description blocks', async () => { + mockHttpGet.mockResolvedValue( + mockLegalRegulatoryResponse([ + sampleResult, + { + url: 'https://www.ecfr.gov/current/title-21/part-102', + title: '21 CFR Part 102', + position: 2, + }, + ]) + ); + + await handleLegalRegulatorySearchCommand({ query: 'food labeling' }); + + const [content] = vi.mocked(writeOutput).mock.calls[0]; + expect(content).toBe( + [ + '## 1. 21 CFR Part 101 -- Food Labeling', + sampleResult.url, + 'Food labeling requirements for packaged foods.', + '', + '## 2. 21 CFR Part 102', + 'https://www.ecfr.gov/current/title-21/part-102', + ].join('\n') + ); + }); + + it('prints a placeholder when there are no results', async () => { + mockHttpGet.mockResolvedValue(mockLegalRegulatoryResponse([])); + + await handleLegalRegulatorySearchCommand({ query: 'no hits' }); + + const [content] = vi.mocked(writeOutput).mock.calls[0]; + expect(content).toBe('(no results)'); + }); + + it('tolerates a success response that omits data', async () => { + mockHttpGet.mockResolvedValue({ data: { success: true } }); + + await handleLegalRegulatorySearchCommand({ query: 'no data field' }); + + const [content] = vi.mocked(writeOutput).mock.calls[0]; + expect(content).toBe('(no results)'); + }); + + it('outputs the raw response as JSON with --json', async () => { + mockHttpGet.mockResolvedValue( + mockLegalRegulatoryResponse([sampleResult]) + ); + + await handleLegalRegulatorySearchCommand({ + query: 'food labeling', + json: true, + }); + + const [content] = vi.mocked(writeOutput).mock.calls[0] as [string]; + expect(JSON.parse(content)).toEqual({ + success: true, + data: { web: [sampleResult] }, + }); + }); + + it('writes to the output file with -o', async () => { + mockHttpGet.mockResolvedValue( + mockLegalRegulatoryResponse([sampleResult]) + ); + + await handleLegalRegulatorySearchCommand({ + query: 'food labeling', + output: 'results.md', + }); + + expect(writeOutput).toHaveBeenCalledWith( + expect.any(String), + 'results.md', + true + ); + }); + }); + + describe('error handling', () => { + it('exits with code 1 when the request fails', async () => { + mockHttpGet.mockRejectedValue(new Error('boom')); + const exitSpy = vi + .spyOn(process, 'exit') + .mockImplementation((() => undefined) as any); + const errorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + + await handleLegalRegulatorySearchCommand({ query: 'test' }); + + expect(errorSpy).toHaveBeenCalledWith('Error:', 'boom'); + expect(exitSpy).toHaveBeenCalledWith(1); + + exitSpy.mockRestore(); + errorSpy.mockRestore(); + }); + }); +}); diff --git a/src/commands/legal-regulatory.ts b/src/commands/legal-regulatory.ts new file mode 100644 index 0000000000..bd4a1ce939 --- /dev/null +++ b/src/commands/legal-regulatory.ts @@ -0,0 +1,78 @@ +import { getClient, isKeylessMode, keylessGet } from '../utils/client'; +import { writeOutput } from '../utils/output'; +import type { + LegalRegulatoryResult, + LegalRegulatorySearchOptions, + LegalRegulatorySearchResponse, +} from '../types/legal-regulatory'; + +const BASE = '/v2/search/legal-regulatory'; + +async function getLegalRegulatory( + path: string, + options: LegalRegulatorySearchOptions +): Promise { + const url = `${path}${path.includes('?') ? '&' : '?'}integration=cli`; + + if (isKeylessMode(options.apiKey, options.apiUrl)) { + return (await keylessGet(url)) as T; + } + + const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); + const response = await (app as any).http.get(url); + return (response?.data ?? {}) as T; +} + +function fmtResult(item: LegalRegulatoryResult, index: number): string { + const lines = [ + `## ${item.position ?? index + 1}. ${item.title ?? '(untitled)'}`, + item.url, + ]; + if (item.description) lines.push(item.description); + return lines.join('\n'); +} + +function fmtLegalRegulatory(data: LegalRegulatorySearchResponse): string { + const results = data.data?.web ?? []; + if (results.length === 0) return '(no results)'; + return results.map(fmtResult).join('\n\n'); +} + +function writeLegalRegulatoryOutput( + data: LegalRegulatorySearchResponse, + readable: string, + options: LegalRegulatorySearchOptions +): void { + const content = + options.json || options.pretty + ? options.pretty + ? JSON.stringify(data, null, 2) + : JSON.stringify(data) + : readable; + writeOutput(content, options.output, !!options.output); +} + +function handleError(error: unknown): never { + console.error( + 'Error:', + error instanceof Error ? error.message : 'Unknown error occurred' + ); + process.exit(1); +} + +export async function handleLegalRegulatorySearchCommand( + options: LegalRegulatorySearchOptions +): Promise { + try { + const params = new URLSearchParams(); + params.append('query', options.query); + if (options.k != null) params.append('k', String(options.k)); + const data = await getLegalRegulatory( + `${BASE}?${params.toString()}`, + options + ); + writeLegalRegulatoryOutput(data, fmtLegalRegulatory(data), options); + } catch (error) { + handleError(error); + } +} diff --git a/src/commands/list.ts b/src/commands/list.ts index c6ef65d59b..3a131db4f7 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -175,8 +175,9 @@ function renderCategories(items: Category[]): string { ...items.map((item) => ` ${item.name} (${item.id}): ${item.description}`), ...(!items.length ? [' No categories are currently visible.'] : []), '', - 'Developer and Research indexes have native commands:', + 'Developer, Legal and Regulatory, and Research indexes have native commands:', ' firecrawl developer --help', + ' firecrawl legal-regulatory --help', ' firecrawl research --help', '', 'All providers: firecrawl alexandria list --providers', diff --git a/src/index.ts b/src/index.ts index aef4eb4bd0..1d986ff380 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ import { handleParseCommand } from './commands/parse'; import { createMonitorCommand } from './commands/monitor'; import { handleSearchCommand } from './commands/search'; import { handleDeveloperSearchCommand } from './commands/developer'; +import { handleLegalRegulatorySearchCommand } from './commands/legal-regulatory'; import { handleInspectPaperCommand, handleReadPaperCommand, @@ -1163,6 +1164,52 @@ Examples: return developerCmd; } +/** + * Create and configure the legal-regulatory command + */ +function createLegalRegulatoryCommand(): Command { + const legalRegulatoryCmd = new Command('legal-regulatory') + .description( + 'Search the Legal and Regulatory Index: primary law and regulatory material from US federal, state, and local government sources, including statutes, regulations, codes, court opinions, and other government publications.' + ) + .argument('', 'Natural-language legal question or search phrase') + .option( + '--limit ', + 'Number of results to return (default: 10, max: 100)', + parseInt + ) + .addOption(new Option('--k ').argParser(parseInt).hideHelp()) + .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 compact JSON', false) + .option('--pretty', 'Pretty print JSON output', false) + .addHelpText( + 'after', + ` +Examples: + $ firecrawl legal-regulatory "food labeling requirements for allergens" --limit 10 + $ firecrawl legal-regulatory "California data breach notification statute" --json +` + ) + .action(async (query, options) => { + await handleLegalRegulatorySearchCommand({ + query, + k: researchLimit(options), + apiKey: options.apiKey, + apiUrl: options.apiUrl, + output: options.output, + json: options.json, + pretty: options.pretty, + }); + }); + + return legalRegulatoryCmd; +} + /** * Create and configure the research command group */ @@ -2235,6 +2282,7 @@ program.addCommand(createFindToolsCommand()); program.addCommand(createListCommand()); program.addCommand(createAlexandriaCommand()); program.addCommand(createDeveloperCommand()); +program.addCommand(createLegalRegulatoryCommand()); program.addCommand(createResearchCommand()); program.addCommand(createFeedbackCommand()); program.addCommand(createSearchFeedbackCommand()); diff --git a/src/types/legal-regulatory.ts b/src/types/legal-regulatory.ts new file mode 100644 index 0000000000..a1f050b5fb --- /dev/null +++ b/src/types/legal-regulatory.ts @@ -0,0 +1,21 @@ +export interface LegalRegulatorySearchOptions { + query: string; + k?: number; + apiKey?: string; + apiUrl?: string; + output?: string; + json?: boolean; + pretty?: boolean; +} + +export interface LegalRegulatoryResult { + url: string; + title?: string; + description?: string; + position?: number; +} + +export interface LegalRegulatorySearchResponse { + success: boolean; + data?: { web?: LegalRegulatoryResult[] }; +} From 2e4540c74892eeaba0283c5faa35b36e840936f3 Mon Sep 17 00:00:00 2001 From: PhantomInTheWire <135950363+PhantomInTheWire@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:11:21 +0000 Subject: [PATCH 2/9] refactor(index): move the endpoint to /search/gov --- src/__tests__/commands/legal-regulatory.test.ts | 6 +++--- src/commands/legal-regulatory.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/__tests__/commands/legal-regulatory.test.ts b/src/__tests__/commands/legal-regulatory.test.ts index 21b2ef3f8c..aa9b1f068a 100644 --- a/src/__tests__/commands/legal-regulatory.test.ts +++ b/src/__tests__/commands/legal-regulatory.test.ts @@ -53,7 +53,7 @@ describe('handleLegalRegulatorySearchCommand', () => { }); describe('API call generation', () => { - it('calls /v2/search/legal-regulatory with the query', async () => { + it('calls /v2/search/gov with the query', async () => { mockHttpGet.mockResolvedValue( mockLegalRegulatoryResponse([sampleResult]) ); @@ -62,7 +62,7 @@ describe('handleLegalRegulatorySearchCommand', () => { expect(mockHttpGet).toHaveBeenCalledTimes(1); expect(mockHttpGet).toHaveBeenCalledWith( - '/v2/search/legal-regulatory?query=food+labeling&integration=cli' + '/v2/search/gov?query=food+labeling&integration=cli' ); }); @@ -77,7 +77,7 @@ describe('handleLegalRegulatorySearchCommand', () => { }); expect(mockHttpGet).toHaveBeenCalledWith( - '/v2/search/legal-regulatory?query=food+labeling&k=5&integration=cli' + '/v2/search/gov?query=food+labeling&k=5&integration=cli' ); }); diff --git a/src/commands/legal-regulatory.ts b/src/commands/legal-regulatory.ts index bd4a1ce939..0ed02ac9c4 100644 --- a/src/commands/legal-regulatory.ts +++ b/src/commands/legal-regulatory.ts @@ -6,7 +6,7 @@ import type { LegalRegulatorySearchResponse, } from '../types/legal-regulatory'; -const BASE = '/v2/search/legal-regulatory'; +const BASE = '/v2/search/gov'; async function getLegalRegulatory( path: string, From 7dd360b9123891f40eb923c08e0d166f3e449f14 Mon Sep 17 00:00:00 2001 From: PhantomInTheWire <135950363+PhantomInTheWire@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:22:31 +0000 Subject: [PATCH 3/9] fix(index): exit non-zero on an unsuccessful response --- .../commands/legal-regulatory.test.ts | 50 ++++++++++++++++++- src/commands/legal-regulatory.ts | 3 ++ src/types/legal-regulatory.ts | 1 + 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/__tests__/commands/legal-regulatory.test.ts b/src/__tests__/commands/legal-regulatory.test.ts index aa9b1f068a..56324fdf74 100644 --- a/src/__tests__/commands/legal-regulatory.test.ts +++ b/src/__tests__/commands/legal-regulatory.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { handleLegalRegulatorySearchCommand } from '../../commands/legal-regulatory'; -import { getClient } from '../../utils/client'; +import { getClient, isKeylessMode } from '../../utils/client'; import { initializeConfig } from '../../utils/config'; import { writeOutput } from '../../utils/output'; import { setupTest, teardownTest } from '../utils/mock-client'; @@ -16,6 +16,7 @@ vi.mock('../../utils/client', async () => { return { ...actual, getClient: vi.fn(), + isKeylessMode: vi.fn(() => false), }; }); @@ -50,6 +51,7 @@ describe('handleLegalRegulatorySearchCommand', () => { afterEach(() => { teardownTest(); vi.clearAllMocks(); + vi.unstubAllGlobals(); }); describe('API call generation', () => { @@ -178,7 +180,53 @@ describe('handleLegalRegulatorySearchCommand', () => { }); }); + describe('keyless mode', () => { + it('calls the endpoint directly and renders the results', async () => { + vi.mocked(isKeylessMode).mockReturnValueOnce(true); + const fetchMock = vi.fn( + async (_url: string, _init?: RequestInit) => + new Response( + JSON.stringify({ success: true, data: { web: [sampleResult] } }), + { status: 200 } + ) + ); + vi.stubGlobal('fetch', fetchMock); + + await handleLegalRegulatorySearchCommand({ query: 'food labeling' }); + + expect(mockHttpGet).not.toHaveBeenCalled(); + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.firecrawl.dev/v2/search/gov?query=food+labeling&integration=cli', + expect.objectContaining({ method: 'GET' }) + ); + expect(vi.mocked(writeOutput).mock.calls[0][0]).toContain( + sampleResult.title + ); + }); + }); + describe('error handling', () => { + it('exits with code 1 when the response reports a failure', async () => { + mockHttpGet.mockResolvedValue({ + data: { success: false, error: 'Search failed' }, + }); + const exitSpy = vi + .spyOn(process, 'exit') + .mockImplementation((() => undefined) as any); + const errorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + + await handleLegalRegulatorySearchCommand({ query: 'test' }); + + expect(errorSpy).toHaveBeenCalledWith('Error:', 'Search failed'); + expect(exitSpy).toHaveBeenCalledWith(1); + expect(writeOutput).not.toHaveBeenCalled(); + + exitSpy.mockRestore(); + errorSpy.mockRestore(); + }); + it('exits with code 1 when the request fails', async () => { mockHttpGet.mockRejectedValue(new Error('boom')); const exitSpy = vi diff --git a/src/commands/legal-regulatory.ts b/src/commands/legal-regulatory.ts index 0ed02ac9c4..7cf1574e91 100644 --- a/src/commands/legal-regulatory.ts +++ b/src/commands/legal-regulatory.ts @@ -71,6 +71,9 @@ export async function handleLegalRegulatorySearchCommand( `${BASE}?${params.toString()}`, options ); + if (data.success === false) { + throw new Error(data.error ?? 'Legal and regulatory search failed'); + } writeLegalRegulatoryOutput(data, fmtLegalRegulatory(data), options); } catch (error) { handleError(error); diff --git a/src/types/legal-regulatory.ts b/src/types/legal-regulatory.ts index a1f050b5fb..2846657075 100644 --- a/src/types/legal-regulatory.ts +++ b/src/types/legal-regulatory.ts @@ -17,5 +17,6 @@ export interface LegalRegulatoryResult { export interface LegalRegulatorySearchResponse { success: boolean; + error?: string; data?: { web?: LegalRegulatoryResult[] }; } From 1e3b2fce4676e67e1d8a899f93ded92b2121da28 Mon Sep 17 00:00:00 2001 From: PhantomInTheWire <135950363+PhantomInTheWire@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:56:47 +0000 Subject: [PATCH 4/9] docs: link the REST reference from the search command --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 42e599a78d..af7468e24d 100644 --- a/README.md +++ b/README.md @@ -413,6 +413,8 @@ firecrawl developer "tokio select cancellation safety" --json -o results.json Search the Legal and Regulatory Index: primary law and regulatory material from US federal, state, and local government sources, including statutes, regulations, codes, court opinions, and other government publications. +For the request and response schema, see the [Legal and Regulatory Index REST API](https://docs.firecrawl.dev/features/legal-regulatory). + ```bash firecrawl legal-regulatory "food labeling requirements for allergens" ``` From 8b1a8ab947594ba297aab1dd877280bac0d9b9f3 Mon Sep 17 00:00:00 2001 From: PhantomInTheWire <135950363+PhantomInTheWire@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:05:15 +0000 Subject: [PATCH 5/9] feat(search): accept the gov category --- README.md | 39 ++++++++++++++------------- skills/firecrawl-search/SKILL.md | 2 +- src/__tests__/commands/search.test.ts | 17 ++++++++++++ src/index.ts | 4 +-- src/types/search.ts | 4 +-- 5 files changed, 43 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index af7468e24d..425e08fd4e 100644 --- a/README.md +++ b/README.md @@ -299,7 +299,7 @@ firecrawl search "landscape photography" --sources images # Multiple sources firecrawl search "machine learning" --sources web,news,images -# Filter by category (research-affiliated websites, PDFs, developer index) +# Filter by category (research-affiliated websites, PDFs, developer index, gov index) firecrawl search "transformer architecture" --categories research firecrawl search "machine learning" --categories pdf,research @@ -309,6 +309,9 @@ firecrawl search "machine learning" --categories pdf,research # Developer search: public repositories, GitHub issues, merged PRs, READMEs, and docs firecrawl search "axum middleware ordering" --categories developer +# Legal and regulatory search: US government sources (cannot be combined with other categories) +firecrawl search "California data breach notification statute" --categories gov + # Time-based search firecrawl search "AI announcements" --tbs qdr:d # Past day firecrawl search "tech news" --tbs qdr:w # Past week @@ -327,23 +330,23 @@ firecrawl search "AI data tools" #### Search Options -| Option | Description | -| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--limit ` | Maximum results (default: 5, max: 100) | -| `--sources ` | Comma-separated: `web`, `images`, `news` (default: web) | -| `--categories ` | Comma-separated: `research` (research-affiliated websites -- for papers use [`research search-papers`](#research---search-research-papers)), `pdf`, `developer` | -| `--tbs ` | Time filter: `qdr:h` (hour), `qdr:d` (day), `qdr:w` (week), `qdr:m` (month), `qdr:y` (year) | -| `--location ` | Geo-targeting (e.g., "Germany", "San Francisco,California,United States") | -| `--country ` | ISO country code (default: US) | -| `--timeout ` | Timeout in milliseconds (default: 60000) | -| `--highlights` | Query-relevant highlights for web and news when available (default) | -| `--no-highlights` | Keep the original search snippets | -| `--ignore-invalid-urls` | Exclude URLs invalid for other Firecrawl endpoints | -| `--scrape` | Enable scraping of search results | -| `--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 | +| Option | Description | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--limit ` | Maximum results (default: 5, max: 100) | +| `--sources ` | Comma-separated: `web`, `images`, `news` (default: web) | +| `--categories ` | Comma-separated: `research` (research-affiliated websites -- for papers use [`research search-papers`](#research---search-research-papers)), `pdf`, `developer`, `gov` (cannot be combined with other categories) | +| `--tbs ` | Time filter: `qdr:h` (hour), `qdr:d` (day), `qdr:w` (week), `qdr:m` (month), `qdr:y` (year) | +| `--location ` | Geo-targeting (e.g., "Germany", "San Francisco,California,United States") | +| `--country ` | ISO country code (default: US) | +| `--timeout ` | Timeout in milliseconds (default: 60000) | +| `--highlights` | Query-relevant highlights for web and news when available (default) | +| `--no-highlights` | Keep the original search snippets | +| `--ignore-invalid-urls` | Exclude URLs invalid for other Firecrawl endpoints | +| `--scrape` | Enable scraping of search results | +| `--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 | #### Examples diff --git a/skills/firecrawl-search/SKILL.md b/skills/firecrawl-search/SKILL.md index 578badaa97..f0b85dfb5e 100644 --- a/skills/firecrawl-search/SKILL.md +++ b/skills/firecrawl-search/SKILL.md @@ -27,7 +27,7 @@ firecrawl search "your query" --sources news --tbs qdr:d -o .firecrawl/news.json Use `firecrawl search --help` for search options, `firecrawl list --help` for contract browsing, and `firecrawl scrape --help` for execution options. -`--categories developer` searches an index of public repositories, GitHub issues, merged pull requests, repository READMEs, and curated documentation sites. `--categories research` is a website filter, not the paper index. Dedicated skills: [firecrawl-developer-index](../firecrawl-developer-index/SKILL.md) and [firecrawl-research-index](../firecrawl-research-index/SKILL.md). +`--categories developer` searches an index of public repositories, GitHub issues, merged pull requests, repository READMEs, and curated documentation sites. `--categories gov` searches US federal, state, and local government legal and regulatory sources and cannot be combined with other categories. `--categories research` is a website filter, not the paper index. Dedicated skills: [firecrawl-developer-index](../firecrawl-developer-index/SKILL.md) and [firecrawl-research-index](../firecrawl-research-index/SKILL.md). **Done when:** relevant results have been inspected, per-call errors and empty results have been checked, the request has been answered with source links, and feedback is sent within the time window unless opted out. diff --git a/src/__tests__/commands/search.test.ts b/src/__tests__/commands/search.test.ts index 246e102017..c6d5da79fd 100644 --- a/src/__tests__/commands/search.test.ts +++ b/src/__tests__/commands/search.test.ts @@ -258,6 +258,23 @@ describe('executeSearch', () => { ); }); + it('should include the gov category when provided', async () => { + mockHttpPost.mockResolvedValue(mockSearchResponse({ web: [] })); + + await executeSearch({ + query: 'food labeling requirements', + categories: ['gov'], + }); + + expect(mockHttpPost).toHaveBeenCalledWith( + '/v2/search', + expect.objectContaining({ + query: 'food labeling requirements', + categories: [{ type: 'gov' }], + }) + ); + }); + it('should include multiple categories correctly', async () => { mockHttpPost.mockResolvedValue(mockSearchResponse({ web: [] })); diff --git a/src/index.ts b/src/index.ts index 1d986ff380..56c64e92c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -942,7 +942,7 @@ function createSearchCommand(): Command { ) .option( '--categories ', - 'Comma-separated categories to filter: research, pdf, developer (research filters web results to research-affiliated websites -- it is NOT the paper index; for papers use `firecrawl research search-papers`. developer searches an index of public repositories, GitHub issues, merged PRs, READMEs, and docs)' + 'Comma-separated categories to filter: research, pdf, developer, gov (research filters web results to research-affiliated websites -- it is NOT the paper index; for papers use `firecrawl research search-papers`. developer searches an index of public repositories, GitHub issues, merged PRs, READMEs, and docs. gov searches US federal, state, and local government legal and regulatory sources and cannot be combined with other categories)' ) .option( '--tbs ', @@ -1045,7 +1045,7 @@ function createSearchCommand(): Command { .map((c: string) => c.trim().toLowerCase()) as SearchCategory[]; // Validate categories - const validCategories = ['research', 'pdf', 'developer']; + const validCategories = ['research', 'pdf', 'developer', 'gov']; for (const category of categories) { if (!validCategories.includes(category)) { console.error( diff --git a/src/types/search.ts b/src/types/search.ts index ebf52aa5d8..fe59460979 100644 --- a/src/types/search.ts +++ b/src/types/search.ts @@ -5,7 +5,7 @@ import type { ScrapeFormat } from './scrape'; export type SearchSource = 'web' | 'images' | 'news' | 'alexandria'; -export type SearchCategory = 'research' | 'pdf' | 'developer'; +export type SearchCategory = 'research' | 'pdf' | 'developer' | 'gov'; export interface SearchOptions { domainTools?: boolean; @@ -24,7 +24,7 @@ export interface SearchOptions { limit?: number; /** Sources to search: web, images, news, alexandria (CLI default: web,alexandria) */ sources?: SearchSource[]; - /** Categories to filter results: research, pdf, developer */ + /** Categories to filter results: research, pdf, developer, gov */ categories?: SearchCategory[]; /** Time-based search parameter (e.g., qdr:h, qdr:d, qdr:w, qdr:m, qdr:y) */ tbs?: string; From e15969281d3fb1a594c195b9a26d5f3f4d02474d Mon Sep 17 00:00:00 2001 From: PhantomInTheWire <135950363+PhantomInTheWire@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:05:16 +0000 Subject: [PATCH 6/9] test: drop redundant cases --- src/__tests__/cli-argv.test.ts | 17 --- .../commands/legal-regulatory.test.ts | 113 +++++------------- 2 files changed, 29 insertions(+), 101 deletions(-) diff --git a/src/__tests__/cli-argv.test.ts b/src/__tests__/cli-argv.test.ts index f501553d7a..42b4ddddfc 100644 --- a/src/__tests__/cli-argv.test.ts +++ b/src/__tests__/cli-argv.test.ts @@ -88,19 +88,6 @@ describe('CLI argv parsing', () => { expect(result.stderr).not.toContain('unknown command'); }); - testWithBuiltCli( - 'lists the legal-regulatory command in root help output', - () => { - const result = spawnSync(process.execPath, [cliPath, '--help'], { - cwd: process.cwd(), - encoding: 'utf8', - }); - - expect(result.status).toBe(0); - expect(result.stdout).toMatch(/^\s*legal-regulatory\b/m); - } - ); - testWithBuiltCli( 'parses the legal-regulatory command and shows its help', () => { @@ -115,10 +102,6 @@ describe('CLI argv parsing', () => { expect(result.status).toBe(0); expect(result.stdout).toContain('Usage: firecrawl legal-regulatory'); - expect(result.stdout).toContain('--limit'); - expect(result.stdout).toContain('--json'); - expect(result.stdout).toContain('--output'); - expect(result.stderr).not.toContain('unknown command'); } ); diff --git a/src/__tests__/commands/legal-regulatory.test.ts b/src/__tests__/commands/legal-regulatory.test.ts index 56324fdf74..e88d8eef76 100644 --- a/src/__tests__/commands/legal-regulatory.test.ts +++ b/src/__tests__/commands/legal-regulatory.test.ts @@ -55,47 +55,21 @@ describe('handleLegalRegulatorySearchCommand', () => { }); describe('API call generation', () => { - it('calls /v2/search/gov with the query', async () => { - mockHttpGet.mockResolvedValue( - mockLegalRegulatoryResponse([sampleResult]) - ); - - await handleLegalRegulatorySearchCommand({ query: 'food labeling' }); - - expect(mockHttpGet).toHaveBeenCalledTimes(1); - expect(mockHttpGet).toHaveBeenCalledWith( - '/v2/search/gov?query=food+labeling&integration=cli' - ); - }); - - it('passes k when a result count is provided', async () => { + it.each([ + [{}, '/v2/search/gov?query=food+labeling&integration=cli'], + [{ k: 5 }, '/v2/search/gov?query=food+labeling&k=5&integration=cli'], + ])('calls /v2/search/gov with %o', async (extra, expectedUrl) => { mockHttpGet.mockResolvedValue( mockLegalRegulatoryResponse([sampleResult]) ); await handleLegalRegulatorySearchCommand({ query: 'food labeling', - k: 5, - }); - - expect(mockHttpGet).toHaveBeenCalledWith( - '/v2/search/gov?query=food+labeling&k=5&integration=cli' - ); - }); - - it('passes apiUrl and apiKey to getClient when provided', async () => { - mockHttpGet.mockResolvedValue(mockLegalRegulatoryResponse([])); - - await handleLegalRegulatorySearchCommand({ - query: 'test', - apiKey: 'other-key', - apiUrl: 'http://localhost:3002', + ...extra, }); - expect(getClient).toHaveBeenCalledWith({ - apiKey: 'other-key', - apiUrl: 'http://localhost:3002', - }); + expect(mockHttpGet).toHaveBeenCalledTimes(1); + expect(mockHttpGet).toHaveBeenCalledWith(expectedUrl); }); }); @@ -127,8 +101,11 @@ describe('handleLegalRegulatorySearchCommand', () => { ); }); - it('prints a placeholder when there are no results', async () => { - mockHttpGet.mockResolvedValue(mockLegalRegulatoryResponse([])); + it.each([ + ['empty results', mockLegalRegulatoryResponse([])], + ['a response without data', { data: { success: true } }], + ])('prints a placeholder for %s', async (_label, response) => { + mockHttpGet.mockResolvedValue(response); await handleLegalRegulatorySearchCommand({ query: 'no hits' }); @@ -136,15 +113,6 @@ describe('handleLegalRegulatorySearchCommand', () => { expect(content).toBe('(no results)'); }); - it('tolerates a success response that omits data', async () => { - mockHttpGet.mockResolvedValue({ data: { success: true } }); - - await handleLegalRegulatorySearchCommand({ query: 'no data field' }); - - const [content] = vi.mocked(writeOutput).mock.calls[0]; - expect(content).toBe('(no results)'); - }); - it('outputs the raw response as JSON with --json', async () => { mockHttpGet.mockResolvedValue( mockLegalRegulatoryResponse([sampleResult]) @@ -161,23 +129,6 @@ describe('handleLegalRegulatorySearchCommand', () => { data: { web: [sampleResult] }, }); }); - - it('writes to the output file with -o', async () => { - mockHttpGet.mockResolvedValue( - mockLegalRegulatoryResponse([sampleResult]) - ); - - await handleLegalRegulatorySearchCommand({ - query: 'food labeling', - output: 'results.md', - }); - - expect(writeOutput).toHaveBeenCalledWith( - expect.any(String), - 'results.md', - true - ); - }); }); describe('keyless mode', () => { @@ -206,10 +157,22 @@ describe('handleLegalRegulatorySearchCommand', () => { }); describe('error handling', () => { - it('exits with code 1 when the response reports a failure', async () => { - mockHttpGet.mockResolvedValue({ - data: { success: false, error: 'Search failed' }, - }); + it.each([ + [ + 'the response reports a failure', + () => + mockHttpGet.mockResolvedValue({ + data: { success: false, error: 'Search failed' }, + }), + 'Search failed', + ], + [ + 'the request fails', + () => mockHttpGet.mockRejectedValue(new Error('boom')), + 'boom', + ], + ])('exits with code 1 when %s', async (_label, arrange, message) => { + arrange(); const exitSpy = vi .spyOn(process, 'exit') .mockImplementation((() => undefined) as any); @@ -219,30 +182,12 @@ describe('handleLegalRegulatorySearchCommand', () => { await handleLegalRegulatorySearchCommand({ query: 'test' }); - expect(errorSpy).toHaveBeenCalledWith('Error:', 'Search failed'); + expect(errorSpy).toHaveBeenCalledWith('Error:', message); expect(exitSpy).toHaveBeenCalledWith(1); expect(writeOutput).not.toHaveBeenCalled(); exitSpy.mockRestore(); errorSpy.mockRestore(); }); - - it('exits with code 1 when the request fails', async () => { - mockHttpGet.mockRejectedValue(new Error('boom')); - const exitSpy = vi - .spyOn(process, 'exit') - .mockImplementation((() => undefined) as any); - const errorSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => undefined); - - await handleLegalRegulatorySearchCommand({ query: 'test' }); - - expect(errorSpy).toHaveBeenCalledWith('Error:', 'boom'); - expect(exitSpy).toHaveBeenCalledWith(1); - - exitSpy.mockRestore(); - errorSpy.mockRestore(); - }); }); }); From 4f59463d153cad58e8ef23fd479125e0ef03806c Mon Sep 17 00:00:00 2001 From: PhantomInTheWire <135950363+PhantomInTheWire@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:16:48 +0000 Subject: [PATCH 7/9] test: trim remaining duplicate cases --- src/__tests__/commands/legal-regulatory.test.ts | 7 ++----- src/__tests__/commands/search.test.ts | 17 ----------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/src/__tests__/commands/legal-regulatory.test.ts b/src/__tests__/commands/legal-regulatory.test.ts index e88d8eef76..8e56b7f0d3 100644 --- a/src/__tests__/commands/legal-regulatory.test.ts +++ b/src/__tests__/commands/legal-regulatory.test.ts @@ -101,11 +101,8 @@ describe('handleLegalRegulatorySearchCommand', () => { ); }); - it.each([ - ['empty results', mockLegalRegulatoryResponse([])], - ['a response without data', { data: { success: true } }], - ])('prints a placeholder for %s', async (_label, response) => { - mockHttpGet.mockResolvedValue(response); + it('prints a placeholder when the response has no data', async () => { + mockHttpGet.mockResolvedValue({ data: { success: true } }); await handleLegalRegulatorySearchCommand({ query: 'no hits' }); diff --git a/src/__tests__/commands/search.test.ts b/src/__tests__/commands/search.test.ts index c6d5da79fd..246e102017 100644 --- a/src/__tests__/commands/search.test.ts +++ b/src/__tests__/commands/search.test.ts @@ -258,23 +258,6 @@ describe('executeSearch', () => { ); }); - it('should include the gov category when provided', async () => { - mockHttpPost.mockResolvedValue(mockSearchResponse({ web: [] })); - - await executeSearch({ - query: 'food labeling requirements', - categories: ['gov'], - }); - - expect(mockHttpPost).toHaveBeenCalledWith( - '/v2/search', - expect.objectContaining({ - query: 'food labeling requirements', - categories: [{ type: 'gov' }], - }) - ); - }); - it('should include multiple categories correctly', async () => { mockHttpPost.mockResolvedValue(mockSearchResponse({ web: [] })); From dc4584a2e11eece4014be14cf512eb798caa8967 Mon Sep 17 00:00:00 2001 From: PhantomInTheWire <135950363+PhantomInTheWire@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:42:55 +0000 Subject: [PATCH 8/9] docs: rename the index --- README.md | 10 +++++----- src/commands/legal-regulatory.ts | 2 +- src/commands/list.ts | 2 +- src/index.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 425e08fd4e..ec27ecb96f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🔥 Firecrawl CLI -Command-line interface for Firecrawl. Search, scrape, interact, crawl, map, search research papers, developer sources, and legal and regulatory sources, and run agent jobs directly from your terminal. +Command-line interface for Firecrawl. Search, scrape, interact, crawl, map, search research papers, developer sources, and government sources, and run agent jobs directly from your terminal. ## Installation @@ -309,7 +309,7 @@ firecrawl search "machine learning" --categories pdf,research # Developer search: public repositories, GitHub issues, merged PRs, READMEs, and docs firecrawl search "axum middleware ordering" --categories developer -# Legal and regulatory search: US government sources (cannot be combined with other categories) +# Government search: US government sources (cannot be combined with other categories) firecrawl search "California data breach notification statute" --categories gov # Time-based search @@ -412,11 +412,11 @@ firecrawl developer "tokio select cancellation safety" --json -o results.json --- -### `legal-regulatory` - Search legal and regulatory sources +### `legal-regulatory` - Search the Firecrawl Government Index -Search the Legal and Regulatory Index: primary law and regulatory material from US federal, state, and local government sources, including statutes, regulations, codes, court opinions, and other government publications. +Search the Government Index: primary law and regulatory material from US federal, state, and local government sources, including statutes, regulations, codes, court opinions, and other government publications. -For the request and response schema, see the [Legal and Regulatory Index REST API](https://docs.firecrawl.dev/features/legal-regulatory). +For the request and response schema, see the [Government Index REST API](https://docs.firecrawl.dev/features/legal-regulatory). ```bash firecrawl legal-regulatory "food labeling requirements for allergens" diff --git a/src/commands/legal-regulatory.ts b/src/commands/legal-regulatory.ts index 7cf1574e91..fc2ef92c16 100644 --- a/src/commands/legal-regulatory.ts +++ b/src/commands/legal-regulatory.ts @@ -72,7 +72,7 @@ export async function handleLegalRegulatorySearchCommand( options ); if (data.success === false) { - throw new Error(data.error ?? 'Legal and regulatory search failed'); + throw new Error(data.error ?? 'Government search failed'); } writeLegalRegulatoryOutput(data, fmtLegalRegulatory(data), options); } catch (error) { diff --git a/src/commands/list.ts b/src/commands/list.ts index 3a131db4f7..f519a632ee 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -175,7 +175,7 @@ function renderCategories(items: Category[]): string { ...items.map((item) => ` ${item.name} (${item.id}): ${item.description}`), ...(!items.length ? [' No categories are currently visible.'] : []), '', - 'Developer, Legal and Regulatory, and Research indexes have native commands:', + 'Developer, Government, and Research indexes have native commands:', ' firecrawl developer --help', ' firecrawl legal-regulatory --help', ' firecrawl research --help', diff --git a/src/index.ts b/src/index.ts index 56c64e92c1..ae96cc518d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1170,7 +1170,7 @@ Examples: function createLegalRegulatoryCommand(): Command { const legalRegulatoryCmd = new Command('legal-regulatory') .description( - 'Search the Legal and Regulatory Index: primary law and regulatory material from US federal, state, and local government sources, including statutes, regulations, codes, court opinions, and other government publications.' + 'Search the Firecrawl Government Index: primary law and regulatory material from US federal, state, and local government sources, including statutes, regulations, codes, court opinions, and other government publications.' ) .argument('', 'Natural-language legal question or search phrase') .option( From c94e8fb670788047a54ed8d9a6647b2b587a0138 Mon Sep 17 00:00:00 2001 From: PhantomInTheWire <135950363+PhantomInTheWire@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:45:32 +0000 Subject: [PATCH 9/9] refactor: rename to gov --- README.md | 10 +++--- src/__tests__/cli-argv.test.ts | 23 +++++--------- .../{legal-regulatory.test.ts => gov.test.ts} | 30 ++++++++---------- src/commands/{legal-regulatory.ts => gov.ts} | 31 +++++++++---------- src/commands/list.ts | 2 +- src/index.ts | 18 +++++------ src/types/{legal-regulatory.ts => gov.ts} | 8 ++--- 7 files changed, 54 insertions(+), 68 deletions(-) rename src/__tests__/commands/{legal-regulatory.test.ts => gov.test.ts} (85%) rename src/commands/{legal-regulatory.ts => gov.ts} (69%) rename src/types/{legal-regulatory.ts => gov.ts} (57%) diff --git a/README.md b/README.md index ec27ecb96f..dcfae383e7 100644 --- a/README.md +++ b/README.md @@ -412,14 +412,14 @@ firecrawl developer "tokio select cancellation safety" --json -o results.json --- -### `legal-regulatory` - Search the Firecrawl Government Index +### `gov` - Search the Firecrawl Government Index Search the Government Index: primary law and regulatory material from US federal, state, and local government sources, including statutes, regulations, codes, court opinions, and other government publications. -For the request and response schema, see the [Government Index REST API](https://docs.firecrawl.dev/features/legal-regulatory). +For the request and response schema, see the [Government Index REST API](https://docs.firecrawl.dev/features/gov). ```bash -firecrawl legal-regulatory "food labeling requirements for allergens" +firecrawl gov "food labeling requirements for allergens" ``` #### Options @@ -435,10 +435,10 @@ firecrawl legal-regulatory "food labeling requirements for allergens" ```bash # Find state statutes on a topic -firecrawl legal-regulatory "California data breach notification statute" --limit 10 +firecrawl gov "California data breach notification statute" --limit 10 # Keep the raw response -firecrawl legal-regulatory "FDA food labeling regulations" --json -o results.json +firecrawl gov "FDA food labeling regulations" --json -o results.json ``` --- diff --git a/src/__tests__/cli-argv.test.ts b/src/__tests__/cli-argv.test.ts index 42b4ddddfc..ae82cd5e17 100644 --- a/src/__tests__/cli-argv.test.ts +++ b/src/__tests__/cli-argv.test.ts @@ -88,22 +88,15 @@ describe('CLI argv parsing', () => { expect(result.stderr).not.toContain('unknown command'); }); - testWithBuiltCli( - 'parses the legal-regulatory command and shows its help', - () => { - const result = spawnSync( - process.execPath, - [cliPath, 'legal-regulatory', '--help'], - { - cwd: process.cwd(), - encoding: 'utf8', - } - ); + testWithBuiltCli('parses the gov command and shows its help', () => { + const result = spawnSync(process.execPath, [cliPath, 'gov', '--help'], { + cwd: process.cwd(), + encoding: 'utf8', + }); - expect(result.status).toBe(0); - expect(result.stdout).toContain('Usage: firecrawl legal-regulatory'); - } - ); + expect(result.status).toBe(0); + expect(result.stdout).toContain('Usage: firecrawl gov'); + }); testWithBuiltCli( 'describes default search highlights and public developer coverage', diff --git a/src/__tests__/commands/legal-regulatory.test.ts b/src/__tests__/commands/gov.test.ts similarity index 85% rename from src/__tests__/commands/legal-regulatory.test.ts rename to src/__tests__/commands/gov.test.ts index 8e56b7f0d3..a371483911 100644 --- a/src/__tests__/commands/legal-regulatory.test.ts +++ b/src/__tests__/commands/gov.test.ts @@ -1,9 +1,9 @@ /** - * Tests for legal-regulatory command + * Tests for gov command */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { handleLegalRegulatorySearchCommand } from '../../commands/legal-regulatory'; +import { handleGovSearchCommand } from '../../commands/gov'; import { getClient, isKeylessMode } from '../../utils/client'; import { initializeConfig } from '../../utils/config'; import { writeOutput } from '../../utils/output'; @@ -20,11 +20,11 @@ vi.mock('../../utils/client', async () => { }; }); -describe('handleLegalRegulatorySearchCommand', () => { +describe('handleGovSearchCommand', () => { let mockHttpGet: ReturnType; // Wrap a payload in the axios envelope returned by `client.http.get`. - const mockLegalRegulatoryResponse = (web: any[]) => ({ + const mockGovResponse = (web: any[]) => ({ data: { success: true, data: { web } }, }); @@ -59,11 +59,9 @@ describe('handleLegalRegulatorySearchCommand', () => { [{}, '/v2/search/gov?query=food+labeling&integration=cli'], [{ k: 5 }, '/v2/search/gov?query=food+labeling&k=5&integration=cli'], ])('calls /v2/search/gov with %o', async (extra, expectedUrl) => { - mockHttpGet.mockResolvedValue( - mockLegalRegulatoryResponse([sampleResult]) - ); + mockHttpGet.mockResolvedValue(mockGovResponse([sampleResult])); - await handleLegalRegulatorySearchCommand({ + await handleGovSearchCommand({ query: 'food labeling', ...extra, }); @@ -76,7 +74,7 @@ describe('handleLegalRegulatorySearchCommand', () => { describe('output', () => { it('renders numbered title, url, and description blocks', async () => { mockHttpGet.mockResolvedValue( - mockLegalRegulatoryResponse([ + mockGovResponse([ sampleResult, { url: 'https://www.ecfr.gov/current/title-21/part-102', @@ -86,7 +84,7 @@ describe('handleLegalRegulatorySearchCommand', () => { ]) ); - await handleLegalRegulatorySearchCommand({ query: 'food labeling' }); + await handleGovSearchCommand({ query: 'food labeling' }); const [content] = vi.mocked(writeOutput).mock.calls[0]; expect(content).toBe( @@ -104,18 +102,16 @@ describe('handleLegalRegulatorySearchCommand', () => { it('prints a placeholder when the response has no data', async () => { mockHttpGet.mockResolvedValue({ data: { success: true } }); - await handleLegalRegulatorySearchCommand({ query: 'no hits' }); + await handleGovSearchCommand({ query: 'no hits' }); const [content] = vi.mocked(writeOutput).mock.calls[0]; expect(content).toBe('(no results)'); }); it('outputs the raw response as JSON with --json', async () => { - mockHttpGet.mockResolvedValue( - mockLegalRegulatoryResponse([sampleResult]) - ); + mockHttpGet.mockResolvedValue(mockGovResponse([sampleResult])); - await handleLegalRegulatorySearchCommand({ + await handleGovSearchCommand({ query: 'food labeling', json: true, }); @@ -140,7 +136,7 @@ describe('handleLegalRegulatorySearchCommand', () => { ); vi.stubGlobal('fetch', fetchMock); - await handleLegalRegulatorySearchCommand({ query: 'food labeling' }); + await handleGovSearchCommand({ query: 'food labeling' }); expect(mockHttpGet).not.toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalledWith( @@ -177,7 +173,7 @@ describe('handleLegalRegulatorySearchCommand', () => { .spyOn(console, 'error') .mockImplementation(() => undefined); - await handleLegalRegulatorySearchCommand({ query: 'test' }); + await handleGovSearchCommand({ query: 'test' }); expect(errorSpy).toHaveBeenCalledWith('Error:', message); expect(exitSpy).toHaveBeenCalledWith(1); diff --git a/src/commands/legal-regulatory.ts b/src/commands/gov.ts similarity index 69% rename from src/commands/legal-regulatory.ts rename to src/commands/gov.ts index fc2ef92c16..f92f679c2e 100644 --- a/src/commands/legal-regulatory.ts +++ b/src/commands/gov.ts @@ -1,17 +1,14 @@ import { getClient, isKeylessMode, keylessGet } from '../utils/client'; import { writeOutput } from '../utils/output'; import type { - LegalRegulatoryResult, - LegalRegulatorySearchOptions, - LegalRegulatorySearchResponse, -} from '../types/legal-regulatory'; + GovResult, + GovSearchOptions, + GovSearchResponse, +} from '../types/gov'; const BASE = '/v2/search/gov'; -async function getLegalRegulatory( - path: string, - options: LegalRegulatorySearchOptions -): Promise { +async function getGov(path: string, options: GovSearchOptions): Promise { const url = `${path}${path.includes('?') ? '&' : '?'}integration=cli`; if (isKeylessMode(options.apiKey, options.apiUrl)) { @@ -23,7 +20,7 @@ async function getLegalRegulatory( return (response?.data ?? {}) as T; } -function fmtResult(item: LegalRegulatoryResult, index: number): string { +function fmtResult(item: GovResult, index: number): string { const lines = [ `## ${item.position ?? index + 1}. ${item.title ?? '(untitled)'}`, item.url, @@ -32,16 +29,16 @@ function fmtResult(item: LegalRegulatoryResult, index: number): string { return lines.join('\n'); } -function fmtLegalRegulatory(data: LegalRegulatorySearchResponse): string { +function fmtGov(data: GovSearchResponse): string { const results = data.data?.web ?? []; if (results.length === 0) return '(no results)'; return results.map(fmtResult).join('\n\n'); } -function writeLegalRegulatoryOutput( - data: LegalRegulatorySearchResponse, +function writeGovOutput( + data: GovSearchResponse, readable: string, - options: LegalRegulatorySearchOptions + options: GovSearchOptions ): void { const content = options.json || options.pretty @@ -60,21 +57,21 @@ function handleError(error: unknown): never { process.exit(1); } -export async function handleLegalRegulatorySearchCommand( - options: LegalRegulatorySearchOptions +export async function handleGovSearchCommand( + options: GovSearchOptions ): Promise { try { const params = new URLSearchParams(); params.append('query', options.query); if (options.k != null) params.append('k', String(options.k)); - const data = await getLegalRegulatory( + const data = await getGov( `${BASE}?${params.toString()}`, options ); if (data.success === false) { throw new Error(data.error ?? 'Government search failed'); } - writeLegalRegulatoryOutput(data, fmtLegalRegulatory(data), options); + writeGovOutput(data, fmtGov(data), options); } catch (error) { handleError(error); } diff --git a/src/commands/list.ts b/src/commands/list.ts index f519a632ee..c5c0bb239c 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -177,7 +177,7 @@ function renderCategories(items: Category[]): string { '', 'Developer, Government, and Research indexes have native commands:', ' firecrawl developer --help', - ' firecrawl legal-regulatory --help', + ' firecrawl gov --help', ' firecrawl research --help', '', 'All providers: firecrawl alexandria list --providers', diff --git a/src/index.ts b/src/index.ts index ae96cc518d..343bdcd552 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,7 +28,7 @@ import { handleParseCommand } from './commands/parse'; import { createMonitorCommand } from './commands/monitor'; import { handleSearchCommand } from './commands/search'; import { handleDeveloperSearchCommand } from './commands/developer'; -import { handleLegalRegulatorySearchCommand } from './commands/legal-regulatory'; +import { handleGovSearchCommand } from './commands/gov'; import { handleInspectPaperCommand, handleReadPaperCommand, @@ -1165,10 +1165,10 @@ Examples: } /** - * Create and configure the legal-regulatory command + * Create and configure the gov command */ -function createLegalRegulatoryCommand(): Command { - const legalRegulatoryCmd = new Command('legal-regulatory') +function createGovCommand(): Command { + const govCmd = new Command('gov') .description( 'Search the Firecrawl Government Index: primary law and regulatory material from US federal, state, and local government sources, including statutes, regulations, codes, court opinions, and other government publications.' ) @@ -1191,12 +1191,12 @@ function createLegalRegulatoryCommand(): Command { 'after', ` Examples: - $ firecrawl legal-regulatory "food labeling requirements for allergens" --limit 10 - $ firecrawl legal-regulatory "California data breach notification statute" --json + $ firecrawl gov "food labeling requirements for allergens" --limit 10 + $ firecrawl gov "California data breach notification statute" --json ` ) .action(async (query, options) => { - await handleLegalRegulatorySearchCommand({ + await handleGovSearchCommand({ query, k: researchLimit(options), apiKey: options.apiKey, @@ -1207,7 +1207,7 @@ Examples: }); }); - return legalRegulatoryCmd; + return govCmd; } /** @@ -2282,7 +2282,7 @@ program.addCommand(createFindToolsCommand()); program.addCommand(createListCommand()); program.addCommand(createAlexandriaCommand()); program.addCommand(createDeveloperCommand()); -program.addCommand(createLegalRegulatoryCommand()); +program.addCommand(createGovCommand()); program.addCommand(createResearchCommand()); program.addCommand(createFeedbackCommand()); program.addCommand(createSearchFeedbackCommand()); diff --git a/src/types/legal-regulatory.ts b/src/types/gov.ts similarity index 57% rename from src/types/legal-regulatory.ts rename to src/types/gov.ts index 2846657075..7ccd45f3a2 100644 --- a/src/types/legal-regulatory.ts +++ b/src/types/gov.ts @@ -1,4 +1,4 @@ -export interface LegalRegulatorySearchOptions { +export interface GovSearchOptions { query: string; k?: number; apiKey?: string; @@ -8,15 +8,15 @@ export interface LegalRegulatorySearchOptions { pretty?: boolean; } -export interface LegalRegulatoryResult { +export interface GovResult { url: string; title?: string; description?: string; position?: number; } -export interface LegalRegulatorySearchResponse { +export interface GovSearchResponse { success: boolean; error?: string; - data?: { web?: LegalRegulatoryResult[] }; + data?: { web?: GovResult[] }; }