From 69a65f507bfaefd74d7eec60a308621b5f8e3912 Mon Sep 17 00:00:00 2001 From: Karan Lokchandani Date: Tue, 18 Aug 2026 23:36:47 +0530 Subject: [PATCH 1/3] developer: delegate passage sizing to search server --- README.md | 21 +++++----- src/__tests__/cli-argv.test.ts | 1 + src/__tests__/commands/developer.test.ts | 49 +++++++++++++++++++++--- src/commands/developer.ts | 30 ++++++++++++--- src/index.ts | 7 ++++ src/types/developer.ts | 1 + 6 files changed, 88 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 6576734427..c15f04a1ed 100644 --- a/README.md +++ b/README.md @@ -368,13 +368,16 @@ firecrawl developer "axum middleware ordering" #### Options -| Option | Description | -| --------------------- | ----------------------------------------- | -| `--limit ` | Number of results (default: 10, max: 100) | -| `--skills-only` | Search only agent-skill files | -| `-o, --output ` | Save to file | -| `--json` | Output as compact JSON | -| `--pretty` | Pretty print JSON output | +| Option | Description | +| --------------------------- | --------------------------------------------------------- | +| `--limit ` | Number of results (default: 10, max: 100) | +| `--skills-only` | Search only agent-skill files | +| `--passage-budget ` | Approximate-token budget for all passages (default: 4096) | +| `-o, --output ` | Save to file | +| `--json` | Output as compact JSON | +| `--pretty` | Pretty print JSON output | + +The passage budget accepts 256–16384 tokens and is allocated by the search server across all results. The 4096-token default preserves the intent of the previous readable-output cap: 1200 characters were roughly 300 tokens per result, or about 3000 passage tokens across the default 10 results, with additional allocation headroom. #### Examples @@ -382,8 +385,8 @@ firecrawl developer "axum middleware ordering" # Investigate a known bug firecrawl developer "tokio spawn_blocking panics thread limit" --limit 10 -# Keep the full passages for an agent -firecrawl developer "tokio select cancellation safety" --json -o results.json +# Give the server more passage space for an agent +firecrawl developer "tokio select cancellation safety" --passage-budget 8192 --json -o results.json ``` --- diff --git a/src/__tests__/cli-argv.test.ts b/src/__tests__/cli-argv.test.ts index 8b93a05631..ad58ae121f 100644 --- a/src/__tests__/cli-argv.test.ts +++ b/src/__tests__/cli-argv.test.ts @@ -31,6 +31,7 @@ describe('CLI argv parsing', () => { expect(result.stdout).toContain('Usage: firecrawl developer'); expect(result.stdout).toContain('--limit'); expect(result.stdout).toContain('--skills-only'); + expect(result.stdout).toContain('--passage-budget'); expect(result.stderr).not.toContain('unknown command'); }); diff --git a/src/__tests__/commands/developer.test.ts b/src/__tests__/commands/developer.test.ts index 248246d51b..b20477b727 100644 --- a/src/__tests__/commands/developer.test.ts +++ b/src/__tests__/commands/developer.test.ts @@ -25,8 +25,17 @@ describe('handleDeveloperSearchCommand', () => { // Wrap a payload in the axios envelope returned by `client.http.get`. // Mirrors the `/v2/search/developer` response shape: // { success, results: [{ id, type, url, title, passages: [{ text }] }] } - const mockDeveloperResponse = (results: any[]) => ({ - data: { success: true, results }, + const mockDeveloperResponse = ( + results: any[], + passageBudgetApplied?: number + ) => ({ + data: { + success: true, + results, + ...(passageBudgetApplied == null + ? {} + : { passage_budget_applied: passageBudgetApplied }), + }, }); const sampleResult = { @@ -63,7 +72,7 @@ describe('handleDeveloperSearchCommand', () => { expect(mockHttpGet).toHaveBeenCalledTimes(1); expect(mockHttpGet).toHaveBeenCalledWith( - '/v2/search/developer?query=tokio+spawn_blocking&integration=cli' + '/v2/search/developer?query=tokio+spawn_blocking&passage_budget=4096&integration=cli' ); }); @@ -76,7 +85,7 @@ describe('handleDeveloperSearchCommand', () => { }); expect(mockHttpGet).toHaveBeenCalledWith( - '/v2/search/developer?query=tokio+spawn_blocking&skills=only&integration=cli' + '/v2/search/developer?query=tokio+spawn_blocking&skills=only&passage_budget=4096&integration=cli' ); }); @@ -89,7 +98,20 @@ describe('handleDeveloperSearchCommand', () => { }); expect(mockHttpGet).toHaveBeenCalledWith( - '/v2/search/developer?query=tokio+spawn_blocking&k=5&integration=cli' + '/v2/search/developer?query=tokio+spawn_blocking&k=5&passage_budget=4096&integration=cli' + ); + }); + + it('passes a custom passage budget through verbatim', async () => { + mockHttpGet.mockResolvedValue(mockDeveloperResponse([sampleResult], 768)); + + await handleDeveloperSearchCommand({ + query: 'tokio spawn_blocking', + passageBudget: 768, + }); + + expect(mockHttpGet).toHaveBeenCalledWith( + '/v2/search/developer?query=tokio+spawn_blocking&passage_budget=768&integration=cli' ); }); @@ -125,7 +147,7 @@ describe('handleDeveloperSearchCommand', () => { expect(content).toContain('It will panic if this limit is too low.'); }); - it('joins multiple passages and clips long content', async () => { + it('keeps the legacy local cut when the server omits budget metadata', async () => { mockHttpGet.mockResolvedValue( mockDeveloperResponse([ { @@ -143,6 +165,21 @@ describe('handleDeveloperSearchCommand', () => { expect(body.length).toBeLessThanOrEqual(1200); }); + it('does not cut content after the server applies the passage budget', async () => { + const passage = 'x'.repeat(5000); + mockHttpGet.mockResolvedValue( + mockDeveloperResponse( + [{ ...sampleResult, passages: [{ text: passage }] }], + 4096 + ) + ); + + await handleDeveloperSearchCommand({ query: 'tokio spawn_blocking' }); + + const [content] = vi.mocked(writeOutput).mock.calls[0] as [string]; + expect(content).toContain(passage); + }); + it('prints a placeholder when there are no results', async () => { mockHttpGet.mockResolvedValue(mockDeveloperResponse([])); diff --git a/src/commands/developer.ts b/src/commands/developer.ts index 701d22e242..355cdf6b6d 100644 --- a/src/commands/developer.ts +++ b/src/commands/developer.ts @@ -5,7 +5,8 @@ import type { DeveloperItem, DeveloperSearchOptions } from '../types/developer'; // The other mount, /v2/developer/search, rejects keyless callers and may be // withdrawn. const BASE = '/v2/search/developer'; -const MAX_PASSAGE_CHARS = 1200; +const DEFAULT_PASSAGE_BUDGET = 4096; +const LEGACY_MAX_PASSAGE_CHARS = 1200; async function getDeveloper( path: string, @@ -22,7 +23,10 @@ async function getDeveloper( return (response?.data ?? {}) as T; } -function fmtDeveloper(results?: DeveloperItem[]): string { +function fmtDeveloper( + results?: DeveloperItem[], + passageBudgetApplied?: number +): string { if (!results || results.length === 0) return '(no results)'; return results @@ -36,7 +40,13 @@ function fmtDeveloper(results?: DeveloperItem[]): string { .map((passage) => passage.text ?? '') .join('\n---\n') .trim(); - lines.push(body ? body.slice(0, MAX_PASSAGE_CHARS) : '(no content)'); + // TODO(search#843): Remove this fallback after server passage budgeting + // is fully enabled. + const renderedBody = + passageBudgetApplied == null + ? body.slice(0, LEGACY_MAX_PASSAGE_CHARS) + : body; + lines.push(renderedBody || '(no content)'); return lines.join('\n'); }) .join('\n\n'); @@ -72,11 +82,19 @@ export async function handleDeveloperSearchCommand( params.append('query', options.query); if (options.k != null) params.append('k', String(options.k)); if (options.skillsOnly) params.append('skills', 'only'); - const data = await getDeveloper<{ results?: DeveloperItem[] }>( - `${BASE}?${params.toString()}`, + params.append( + 'passage_budget', + String(options.passageBudget ?? DEFAULT_PASSAGE_BUDGET) + ); + const data = await getDeveloper<{ + results?: DeveloperItem[]; + passage_budget_applied?: number; + }>(`${BASE}?${params.toString()}`, options); + writeDeveloperOutput( + data, + fmtDeveloper(data.results, data.passage_budget_applied), options ); - writeDeveloperOutput(data, fmtDeveloper(data.results), options); } catch (error) { handleError(error); } diff --git a/src/index.ts b/src/index.ts index 87ecfee452..b6ee5e3d2e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1064,6 +1064,12 @@ function createDeveloperCommand(): Command { ) .addOption(new Option('--k ').argParser(parseInt).hideHelp()) .option('--skills-only', 'Search only agent-skill files', false) + .option( + '--passage-budget ', + 'Approximate-token budget for all passage text (default: 4096, range: 256-16384)', + parseInt, + 4096 + ) .option( '-k, --api-key ', 'Firecrawl API key (overrides global --api-key)' @@ -1085,6 +1091,7 @@ Examples: query, k: researchLimit(options), skillsOnly: options.skillsOnly, + passageBudget: options.passageBudget, apiKey: options.apiKey, apiUrl: options.apiUrl, output: options.output, diff --git a/src/types/developer.ts b/src/types/developer.ts index b1fd5b39b1..e721f5a6fb 100644 --- a/src/types/developer.ts +++ b/src/types/developer.ts @@ -2,6 +2,7 @@ export interface DeveloperSearchOptions { query: string; k?: number; skillsOnly?: boolean; + passageBudget?: number; apiKey?: string; apiUrl?: string; output?: string; From f3712e9b7742ddf1504f16fdd6c4dd15af19eed6 Mon Sep 17 00:00:00 2001 From: Karan Lokchandani Date: Tue, 18 Aug 2026 23:44:51 +0530 Subject: [PATCH 2/3] developer: validate passage budget flag --- src/__tests__/cli-argv.test.ts | 18 ++++++++++++++++++ src/index.ts | 12 ++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/__tests__/cli-argv.test.ts b/src/__tests__/cli-argv.test.ts index ad58ae121f..af43391840 100644 --- a/src/__tests__/cli-argv.test.ts +++ b/src/__tests__/cli-argv.test.ts @@ -35,6 +35,24 @@ describe('CLI argv parsing', () => { expect(result.stderr).not.toContain('unknown command'); }); + testWithBuiltCli('rejects an invalid developer passage budget', () => { + for (const budget of ['not-a-number', '255', '16385']) { + const result = spawnSync( + process.execPath, + [cliPath, 'developer', 'query', '--passage-budget', budget], + { + cwd: process.cwd(), + encoding: 'utf8', + } + ); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + 'must be an integer between 256 and 16384' + ); + } + }); + testWithBuiltCli('lists the research command in root help output', () => { const result = spawnSync(process.execPath, [cliPath, '--help'], { cwd: process.cwd(), diff --git a/src/index.ts b/src/index.ts index b6ee5e3d2e..69a5e4e120 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ * Entry point for the CLI application */ -import { Command, Option } from 'commander'; +import { Command, InvalidArgumentError, Option } from 'commander'; import { readFileSync } from 'fs'; import { handleScrapeCommand, @@ -226,6 +226,14 @@ function researchLimit(options: { return options.k ?? options.limit; } +function parsePassageBudget(value: string): number { + const budget = Number(value); + if (!Number.isInteger(budget) || budget < 256 || budget > 16384) { + throw new InvalidArgumentError('must be an integer between 256 and 16384'); + } + return budget; +} + function parseAgentWebhookOption( raw: string | undefined, label: string @@ -1067,7 +1075,7 @@ function createDeveloperCommand(): Command { .option( '--passage-budget ', 'Approximate-token budget for all passage text (default: 4096, range: 256-16384)', - parseInt, + parsePassageBudget, 4096 ) .option( From a9945112cc8d7c7da0d81419de52502bb957d306 Mon Sep 17 00:00:00 2001 From: Karan Lokchandani Date: Thu, 20 Aug 2026 15:23:51 +0530 Subject: [PATCH 3/3] developer: keep passage budget server-side --- README.md | 21 +++++++++------------ src/__tests__/cli-argv.test.ts | 20 +------------------- src/__tests__/commands/developer.test.ts | 21 ++++----------------- src/commands/developer.ts | 5 ----- src/index.ts | 17 +---------------- src/types/developer.ts | 1 - 6 files changed, 15 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index c15f04a1ed..6576734427 100644 --- a/README.md +++ b/README.md @@ -368,16 +368,13 @@ firecrawl developer "axum middleware ordering" #### Options -| Option | Description | -| --------------------------- | --------------------------------------------------------- | -| `--limit ` | Number of results (default: 10, max: 100) | -| `--skills-only` | Search only agent-skill files | -| `--passage-budget ` | Approximate-token budget for all passages (default: 4096) | -| `-o, --output ` | Save to file | -| `--json` | Output as compact JSON | -| `--pretty` | Pretty print JSON output | - -The passage budget accepts 256–16384 tokens and is allocated by the search server across all results. The 4096-token default preserves the intent of the previous readable-output cap: 1200 characters were roughly 300 tokens per result, or about 3000 passage tokens across the default 10 results, with additional allocation headroom. +| Option | Description | +| --------------------- | ----------------------------------------- | +| `--limit ` | Number of results (default: 10, max: 100) | +| `--skills-only` | Search only agent-skill files | +| `-o, --output ` | Save to file | +| `--json` | Output as compact JSON | +| `--pretty` | Pretty print JSON output | #### Examples @@ -385,8 +382,8 @@ The passage budget accepts 256–16384 tokens and is allocated by the search ser # Investigate a known bug firecrawl developer "tokio spawn_blocking panics thread limit" --limit 10 -# Give the server more passage space for an agent -firecrawl developer "tokio select cancellation safety" --passage-budget 8192 --json -o results.json +# Keep the full passages for an agent +firecrawl developer "tokio select cancellation safety" --json -o results.json ``` --- diff --git a/src/__tests__/cli-argv.test.ts b/src/__tests__/cli-argv.test.ts index af43391840..e795a09282 100644 --- a/src/__tests__/cli-argv.test.ts +++ b/src/__tests__/cli-argv.test.ts @@ -31,28 +31,10 @@ describe('CLI argv parsing', () => { expect(result.stdout).toContain('Usage: firecrawl developer'); expect(result.stdout).toContain('--limit'); expect(result.stdout).toContain('--skills-only'); - expect(result.stdout).toContain('--passage-budget'); + expect(result.stdout).not.toContain('--passage-budget'); expect(result.stderr).not.toContain('unknown command'); }); - testWithBuiltCli('rejects an invalid developer passage budget', () => { - for (const budget of ['not-a-number', '255', '16385']) { - const result = spawnSync( - process.execPath, - [cliPath, 'developer', 'query', '--passage-budget', budget], - { - cwd: process.cwd(), - encoding: 'utf8', - } - ); - - expect(result.status).not.toBe(0); - expect(result.stderr).toContain( - 'must be an integer between 256 and 16384' - ); - } - }); - testWithBuiltCli('lists the research command in root help output', () => { const result = spawnSync(process.execPath, [cliPath, '--help'], { cwd: process.cwd(), diff --git a/src/__tests__/commands/developer.test.ts b/src/__tests__/commands/developer.test.ts index b20477b727..c4ba98b864 100644 --- a/src/__tests__/commands/developer.test.ts +++ b/src/__tests__/commands/developer.test.ts @@ -65,14 +65,14 @@ describe('handleDeveloperSearchCommand', () => { }); describe('API call generation', () => { - it('calls /v2/search/developer with the query and integration tag', async () => { + it('calls /v2/search/developer without a client passage budget', async () => { mockHttpGet.mockResolvedValue(mockDeveloperResponse([sampleResult])); await handleDeveloperSearchCommand({ query: 'tokio spawn_blocking' }); expect(mockHttpGet).toHaveBeenCalledTimes(1); expect(mockHttpGet).toHaveBeenCalledWith( - '/v2/search/developer?query=tokio+spawn_blocking&passage_budget=4096&integration=cli' + '/v2/search/developer?query=tokio+spawn_blocking&integration=cli' ); }); @@ -85,7 +85,7 @@ describe('handleDeveloperSearchCommand', () => { }); expect(mockHttpGet).toHaveBeenCalledWith( - '/v2/search/developer?query=tokio+spawn_blocking&skills=only&passage_budget=4096&integration=cli' + '/v2/search/developer?query=tokio+spawn_blocking&skills=only&integration=cli' ); }); @@ -98,20 +98,7 @@ describe('handleDeveloperSearchCommand', () => { }); expect(mockHttpGet).toHaveBeenCalledWith( - '/v2/search/developer?query=tokio+spawn_blocking&k=5&passage_budget=4096&integration=cli' - ); - }); - - it('passes a custom passage budget through verbatim', async () => { - mockHttpGet.mockResolvedValue(mockDeveloperResponse([sampleResult], 768)); - - await handleDeveloperSearchCommand({ - query: 'tokio spawn_blocking', - passageBudget: 768, - }); - - expect(mockHttpGet).toHaveBeenCalledWith( - '/v2/search/developer?query=tokio+spawn_blocking&passage_budget=768&integration=cli' + '/v2/search/developer?query=tokio+spawn_blocking&k=5&integration=cli' ); }); diff --git a/src/commands/developer.ts b/src/commands/developer.ts index 355cdf6b6d..146d11e139 100644 --- a/src/commands/developer.ts +++ b/src/commands/developer.ts @@ -5,7 +5,6 @@ import type { DeveloperItem, DeveloperSearchOptions } from '../types/developer'; // The other mount, /v2/developer/search, rejects keyless callers and may be // withdrawn. const BASE = '/v2/search/developer'; -const DEFAULT_PASSAGE_BUDGET = 4096; const LEGACY_MAX_PASSAGE_CHARS = 1200; async function getDeveloper( @@ -82,10 +81,6 @@ export async function handleDeveloperSearchCommand( params.append('query', options.query); if (options.k != null) params.append('k', String(options.k)); if (options.skillsOnly) params.append('skills', 'only'); - params.append( - 'passage_budget', - String(options.passageBudget ?? DEFAULT_PASSAGE_BUDGET) - ); const data = await getDeveloper<{ results?: DeveloperItem[]; passage_budget_applied?: number; diff --git a/src/index.ts b/src/index.ts index 69a5e4e120..87ecfee452 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ * Entry point for the CLI application */ -import { Command, InvalidArgumentError, Option } from 'commander'; +import { Command, Option } from 'commander'; import { readFileSync } from 'fs'; import { handleScrapeCommand, @@ -226,14 +226,6 @@ function researchLimit(options: { return options.k ?? options.limit; } -function parsePassageBudget(value: string): number { - const budget = Number(value); - if (!Number.isInteger(budget) || budget < 256 || budget > 16384) { - throw new InvalidArgumentError('must be an integer between 256 and 16384'); - } - return budget; -} - function parseAgentWebhookOption( raw: string | undefined, label: string @@ -1072,12 +1064,6 @@ function createDeveloperCommand(): Command { ) .addOption(new Option('--k ').argParser(parseInt).hideHelp()) .option('--skills-only', 'Search only agent-skill files', false) - .option( - '--passage-budget ', - 'Approximate-token budget for all passage text (default: 4096, range: 256-16384)', - parsePassageBudget, - 4096 - ) .option( '-k, --api-key ', 'Firecrawl API key (overrides global --api-key)' @@ -1099,7 +1085,6 @@ Examples: query, k: researchLimit(options), skillsOnly: options.skillsOnly, - passageBudget: options.passageBudget, apiKey: options.apiKey, apiUrl: options.apiUrl, output: options.output, diff --git a/src/types/developer.ts b/src/types/developer.ts index e721f5a6fb..b1fd5b39b1 100644 --- a/src/types/developer.ts +++ b/src/types/developer.ts @@ -2,7 +2,6 @@ export interface DeveloperSearchOptions { query: string; k?: number; skillsOnly?: boolean; - passageBudget?: number; apiKey?: string; apiUrl?: string; output?: string;