From cd6b2adf0eeaaaa05a91ddcc9de08f65993fee64 Mon Sep 17 00:00:00 2001 From: Eric Ciarla Date: Tue, 15 Sep 2026 11:14:04 -0400 Subject: [PATCH 1/3] feat(cli): preserve and display API agent hints --- README.md | 16 ++ src/__tests__/alexandria-beta.test.ts | 35 +++ src/__tests__/commands/agent-hints.test.ts | 293 +++++++++++++++++++++ src/commands/alexandria.ts | 31 ++- src/commands/map.ts | 40 +-- src/commands/parse.ts | 31 ++- src/commands/scrape.ts | 40 ++- src/commands/search.ts | 66 ++--- src/index.ts | 7 + src/types/map.ts | 7 +- src/types/parse.ts | 5 +- src/types/scrape.ts | 6 +- src/types/search.ts | 5 +- src/utils/agent-hints.ts | 50 ++++ src/utils/client.ts | 10 +- src/utils/options.ts | 1 + src/utils/output.ts | 42 ++- 17 files changed, 596 insertions(+), 89 deletions(-) create mode 100644 src/__tests__/commands/agent-hints.test.ts create mode 100644 src/utils/agent-hints.ts diff --git a/README.md b/README.md index 20170e2d68..2d164f8802 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,22 @@ Command-line interface for Firecrawl. Search, scrape, interact, crawl, map, search research papers and developer sources, and run agent jobs directly from your terminal. +### API response guidance + +Search, Scrape, Parse, Map, and Alexandria commands preserve optional `agent_hints` returned by the API. These are up to three server-authored suggestions for useful next requests or feedback after evaluating a result. The CLI does not generate guidance, execute suggested requests, or submit feedback automatically. + +- JSON output includes `agent_hints` when returned, including empty results and errors. Scrape and Parse retain their existing document-shaped JSON, with hints alongside document fields. +- Readable results and raw page output show hints on stderr, so pipes and saved markdown/HTML stay clean. Capture stderr as well as stdout when an agent uses readable output. +- Pass `--no-agent-hints` to omit hints from CLI output, including JSON. This is local suppression; it does not change server-side feedback preferences or API behavior. + +```bash +firecrawl search "example research" --json +firecrawl scrape https://example.com -o page.md +firecrawl scrape https://example.com --json --no-agent-hints +``` + +SDK release prerequisite: authenticated Scrape, Map, and Alexandria calls require a Firecrawl SDK version that preserves outer response `agent_hints` on document/map/Alexandria results and `SdkError`. The currently pinned SDK `4.40.0` does not preserve all of these fields; upgrade it when that SDK change is released. Search and Parse preserve the HTTP response directly, and keyless Scrape preserves its response envelope. + ## Installation ```bash diff --git a/src/__tests__/alexandria-beta.test.ts b/src/__tests__/alexandria-beta.test.ts index caa21c480c..a8405ac46f 100644 --- a/src/__tests__/alexandria-beta.test.ts +++ b/src/__tests__/alexandria-beta.test.ts @@ -120,6 +120,41 @@ it('preserves mixed search results, tools and billing metadata', async () => { expect(readable.stdout).toContain('series/observations'); }); +it('preserves search hints on empty JSON responses and supports local suppression', async () => { + response = { + success: true, + id: 'search-empty', + data: { web: [] }, + agent_hints: ['After evaluation, submit feedback for search-empty.'], + }; + const args = ['search', 'example', '--sources', 'web', '--json']; + const result = await cli(args); + expect(result.code).toBe(0); + expect(JSON.parse(result.stdout)).toEqual(response); + const suppressed = await cli([...args, '--no-agent-hints']); + expect(suppressed.code).toBe(0); + expect(JSON.parse(suppressed.stdout)).toEqual({ + success: true, + id: 'search-empty', + data: { web: [] }, + }); + expect(requests[0].body).toEqual(requests[1].body); +}); + +it('keeps server-authored search error guidance in JSON', async () => { + status = 400; + response = { + success: false, + error: 'Invalid source', + code: 'INVALID_BODY', + id: 'failed-search', + agent_hints: ['Use a supported search source.'], + }; + const result = await cli(['search', 'example', '--sources', 'web', '--json']); + expect(result.code).toBe(1); + expect(JSON.parse(result.stdout)).toEqual(response); +}); + it('sends provider calls to Scrape with a stable retry ID and preserves the receipt', async () => { const args = [ 'scrape', diff --git a/src/__tests__/commands/agent-hints.test.ts b/src/__tests__/commands/agent-hints.test.ts new file mode 100644 index 0000000000..5a1f6dba1a --- /dev/null +++ b/src/__tests__/commands/agent-hints.test.ts @@ -0,0 +1,293 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { handleSearchCommand } from '../../commands/search'; +import { handleScrapeCommand } from '../../commands/scrape'; +import { handleMapCommand } from '../../commands/map'; +import { handleParseCommand } from '../../commands/parse'; +import { handleAlexandria } from '../../commands/alexandria'; +import { getClient, isKeylessMode, keylessRequest } from '../../utils/client'; +import { initializeConfig, resetConfig } from '../../utils/config'; +import { agentHintMetadata } from '../../utils/agent-hints'; + +vi.mock('../../utils/client', async () => ({ + ...(await vi.importActual('../../utils/client')), + getClient: vi.fn(), + isKeylessMode: vi.fn(() => false), + keylessRequest: vi.fn(), +})); + +describe('server agent hints', () => { + const hints = [ + 'Inspect the returned tool definition.', + 'Submit feedback after evaluation.', + ]; + let directory: string; + let post: ReturnType; + let scrape: ReturnType; + let map: ReturnType; + let stdout: string[]; + let stderr: string[]; + let priorExitCode: typeof process.exitCode; + + beforeEach(() => { + directory = mkdtempSync(join(tmpdir(), 'firecrawl-hints-')); + stdout = []; + stderr = []; + priorExitCode = process.exitCode; + post = vi.fn(); + scrape = vi.fn(); + map = vi.fn(); + vi.mocked(getClient).mockReturnValue({ + http: { post }, + scrape, + map, + } as any); + vi.mocked(isKeylessMode).mockReturnValue(false); + initializeConfig({ + apiKey: 'fc-test', + apiUrl: 'https://api.firecrawl.dev', + }); + vi.spyOn(process.stdout, 'write').mockImplementation((chunk: any) => { + stdout.push(String(chunk)); + return true; + }); + vi.spyOn(process.stderr, 'write').mockImplementation((chunk: any) => { + stderr.push(String(chunk)); + return true; + }); + vi.spyOn(console, 'error').mockImplementation((...args: any[]) => { + stderr.push(args.join(' ')); + }); + }); + + afterEach(() => { + process.exitCode = priorExitCode; + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + vi.clearAllMocks(); + resetConfig(); + rmSync(directory, { recursive: true, force: true }); + }); + + it('preserves empty search results, feedback identity and hints in JSON', async () => { + const envelope = { + success: true, + data: { web: [] }, + id: 'search-1', + creditsUsed: 0, + agent_hints: hints, + }; + post.mockResolvedValue({ data: envelope }); + await handleSearchCommand({ query: 'example', json: true }); + expect(JSON.parse(stdout.join(''))).toEqual(envelope); + expect(stderr).toEqual([]); + }); + + it('keeps search hints out of readable results and output files', async () => { + post.mockResolvedValue({ + data: { + success: true, + data: { web: [{ url: 'https://example.com', title: 'Example' }] }, + agent_hints: hints, + }, + }); + const output = join(directory, 'search.txt'); + await handleSearchCommand({ query: 'example', output }); + expect(readFileSync(output, 'utf8')).toContain('Example'); + expect(readFileSync(output, 'utf8')).not.toContain(hints[0]); + expect(stderr.join('')).toContain(hints[0]); + expect(stdout).toEqual([]); + }); + + it('returns JSON on typed search failure with its hints and identity', async () => { + const failure = { + success: false, + error: 'Invalid source', + code: 'INVALID_BODY', + id: 'search-2', + agent_hints: hints, + }; + post.mockRejectedValue( + Object.assign(new Error('Invalid source'), { + response: { data: failure }, + }) + ); + await handleSearchCommand({ query: 'example', json: true }); + expect(JSON.parse(stdout.join(''))).toEqual(failure); + expect(process.exitCode).toBe(1); + }); + + it('can suppress hints in JSON without adding a server request option', async () => { + post.mockResolvedValue({ + data: { success: true, data: { web: [] }, agent_hints: hints }, + }); + await handleSearchCommand({ + query: 'example', + json: true, + agentHints: false, + }); + expect(JSON.parse(stdout.join(''))).toEqual({ + success: true, + data: { web: [] }, + }); + expect(post.mock.calls[0][1]).not.toHaveProperty('agentHints'); + expect(stderr).toEqual([]); + }); + + it('leaves ordinary scrape content unchanged when no hints are returned', async () => { + scrape.mockResolvedValue({ markdown: '# Page' }); + await handleScrapeCommand({ url: 'https://example.com' }); + expect(stdout.join('')).toBe('# Page\n'); + expect(stderr).toEqual([]); + }); + + it('retains SDK hints in flattened scrape JSON and multiple-format JSON', async () => { + scrape.mockResolvedValue({ + markdown: '# Page', + links: ['https://example.com'], + agent_hints: hints, + }); + await handleScrapeCommand({ + url: 'https://example.com', + formats: ['markdown', 'links'], + }); + expect(JSON.parse(stdout.join(''))).toEqual({ + markdown: '# Page', + links: ['https://example.com'], + agent_hints: hints, + }); + expect(stderr).toEqual([]); + }); + + it('preserves keyless envelope hints while writing only page text to a raw file', async () => { + vi.mocked(isKeylessMode).mockReturnValue(true); + vi.mocked(keylessRequest).mockResolvedValue({ + success: true, + data: { markdown: '# Page' }, + agent_hints: hints, + }); + const output = join(directory, 'page.md'); + await handleScrapeCommand({ url: 'https://example.com', output }); + expect(readFileSync(output, 'utf8')).toBe('# Page'); + expect(stderr.join('')).toContain(hints[0]); + }); + + it('honors hint suppression for SDK scrape results and query output', async () => { + scrape.mockResolvedValue({ answer: '42', agent_hints: hints }); + await handleScrapeCommand({ + url: 'https://example.com', + query: 'How many?', + agentHints: false, + }); + expect(stdout.join('')).toBe('42\n'); + expect(stderr).toEqual([]); + }); + + it('preserves a keyless failure envelope even when HTTP succeeded', async () => { + vi.mocked(isKeylessMode).mockReturnValue(true); + const failure = { + success: false, + error: 'Scrape failed', + code: 'SCRAPE_FAILED', + agent_hints: hints, + }; + vi.mocked(keylessRequest).mockResolvedValue(failure); + await handleScrapeCommand({ url: 'https://example.com', json: true }); + expect(JSON.parse(stdout.join(''))).toEqual(failure); + expect(process.exitCode).toBe(1); + }); + + it('keeps query-mode metadata when JSON was explicitly requested', async () => { + scrape.mockResolvedValue({ answer: '42', agent_hints: hints }); + await handleScrapeCommand({ + url: 'https://example.com', + query: 'How many?', + json: true, + }); + expect(JSON.parse(stdout.join(''))).toEqual({ + answer: '42', + agent_hints: hints, + }); + }); + + it('retains error hints exposed by the SDK and returns a failing exit code', async () => { + scrape.mockRejectedValue( + Object.assign(new Error('Request failed'), { agent_hints: hints }) + ); + await handleScrapeCommand({ url: 'https://example.com', json: true }); + expect(JSON.parse(stdout.join(''))).toEqual({ + success: false, + error: 'Request failed', + agent_hints: hints, + }); + expect(process.exitCode).toBe(1); + }); + + it('preserves map hints with an empty link list', async () => { + map.mockResolvedValue({ id: 'map-1', links: [], agent_hints: hints }); + await handleMapCommand({ urlOrJobId: 'https://example.com', json: true }); + expect(JSON.parse(stdout.join(''))).toEqual({ + success: true, + id: 'map-1', + data: { links: [] }, + agent_hints: hints, + }); + }); + + it.each([true, false])( + 'preserves parse hints on success=%s', + async (success) => { + const file = join(directory, 'page.html'); + writeFileSync(file, '

Page

'); + const envelope = success + ? { success, data: { markdown: '# Page' }, agent_hints: hints } + : { success, error: 'Parse failed', agent_hints: hints }; + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: success, + status: success ? 200 : 400, + json: async () => envelope, + }) + ); + await handleParseCommand({ file, json: true }); + expect(JSON.parse(stdout.join(''))).toEqual( + success ? { markdown: '# Page', agent_hints: hints } : envelope + ); + } + ); + + it('preserves Alexandria receipt, partial results and SDK hints', async () => { + const alexandria = [ + { provider: 'example', capability: 'lookup', error: { code: 'FAILED' } }, + ]; + scrape.mockResolvedValue({ + alexandria, + creditsCost: 1, + scrapeId: 'scrape-3', + agent_hints: hints, + }); + await handleAlexandria( + [{ provider: 'example', capability: 'lookup', options: {} }], + { requestId: 'retry-1' } + ); + expect(JSON.parse(stdout.join(''))).toEqual({ + success: true, + scrape_id: 'scrape-3', + data: { alexandria, creditsCost: 1 }, + requestId: 'retry-1', + agent_hints: hints, + }); + expect(process.exitCode).toBe(1); + }); + + it('accepts only string hints and caps malformed upstream arrays at three', () => { + expect( + agentHintMetadata({ agent_hints: ['one', null, 'two', 'three', 'four'] }) + ).toEqual({ agent_hints: ['one', 'two', 'three'] }); + expect(agentHintMetadata({ agent_hints: 'not an array' })).toEqual({}); + expect(agentHintMetadata({})).toEqual({}); + }); +}); diff --git a/src/commands/alexandria.ts b/src/commands/alexandria.ts index 7cb350027c..f1586533ba 100644 --- a/src/commands/alexandria.ts +++ b/src/commands/alexandria.ts @@ -4,9 +4,15 @@ import { SdkError, type AlexandriaCall } from 'firecrawl'; import { getClient } from '../utils/client'; import { getApiKey } from '../utils/config'; import { writeOutput } from '../utils/output'; +import { + agentHintMetadata, + errorAgentHints, + type AgentHintMetadata, + type AgentHintOptions, +} from '../utils/agent-hints'; type Call = AlexandriaCall & { options: Record }; -type Options = { +type Options = AgentHintOptions & { apiKey?: string; apiUrl?: string; requestId?: string; @@ -51,7 +57,20 @@ export function buildCalls(addresses: string[], values: string[] = []): Call[] { }); } -export function apiFailure(error: unknown): Record { +interface ApiFailure extends AgentHintMetadata { + success: false; + error: string; + code?: string; + chargeId?: string; + requiresAction?: unknown; + id?: string; + scrape_id?: string; +} + +export function apiFailure( + error: unknown, + fallback = 'Request failed' +): ApiFailure { const body = (error as any)?.response?.data ?? (error instanceof SdkError @@ -70,10 +89,13 @@ export function apiFailure(error: unknown): Record { ? body.error : error instanceof Error ? error.message - : 'Request failed', + : fallback, ...(typeof body?.code === 'string' && { code: body.code }), ...(typeof body?.chargeId === 'string' && { chargeId: body.chargeId }), ...(body?.requiresAction && { requiresAction: body.requiresAction }), + ...(typeof body?.id === 'string' && { id: body.id }), + ...(typeof body?.scrape_id === 'string' && { scrape_id: body.scrape_id }), + ...errorAgentHints(error), }; } @@ -98,6 +120,7 @@ export async function handleAlexandria( }); envelope = { success: true, + ...agentHintMetadata(result), ...(result.scrapeId && { scrape_id: result.scrapeId }), data: { alexandria: result.alexandria, @@ -111,6 +134,7 @@ export async function handleAlexandria( !envelope.success || envelope.data?.alexandria?.some((item: any) => item.error); if (failed) process.exitCode = 1; + if (options.agentHints === false) delete envelope.agent_hints; writeOutput( JSON.stringify( { ...envelope, requestId }, @@ -136,6 +160,7 @@ export function createFindToolsCommand(): Command { .option('-o, --output ', 'Output file') .option('--json', 'Output JSON') .option('--pretty', 'Format JSON') + .option('--no-agent-hints', 'Omit server guidance from CLI output') .action(async (urls: string[], options) => { let call: Call = { provider: 'firecrawl', diff --git a/src/commands/map.ts b/src/commands/map.ts index bbac27be84..63da23d85c 100644 --- a/src/commands/map.ts +++ b/src/commands/map.ts @@ -5,6 +5,12 @@ import type { MapOptions, MapResult } from '../types/map'; import { getClient } from '../utils/client'; import { writeOutput } from '../utils/output'; +import { + agentHintMetadata, + withoutAgentHints, + writeAgentHints, +} from '../utils/agent-hints'; +import { apiFailure } from './alexandria'; /** * Execute map command @@ -43,6 +49,8 @@ export async function executeMap(options: MapOptions): Promise { return { success: true, + ...(mapData.id && { id: mapData.id }), + ...agentHintMetadata(mapData), data: { links: mapData.links.map((link: any) => ({ url: link.url, @@ -52,10 +60,7 @@ export async function executeMap(options: MapOptions): Promise { }, }; } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - }; + return apiFailure(error, 'Unknown error occurred'); } } @@ -73,7 +78,20 @@ function formatMapReadable(data: MapResult['data']): string { * Handle map command output */ export async function handleMapCommand(options: MapOptions): Promise { - const result = await executeMap(options); + const response = await executeMap(options); + const result = + options.agentHints === false ? withoutAgentHints(response) : response; + + if (options.json) { + writeOutput( + JSON.stringify(result, null, options.pretty ? 2 : undefined), + options.output, + !!options.output + ); + if (!result.success) process.exitCode = 1; + return; + } + writeAgentHints(result); if (!result.success) { console.error('Error:', result.error); @@ -84,17 +102,7 @@ export async function handleMapCommand(options: MapOptions): Promise { return; } - let outputContent: string; - - // Use JSON format if --json flag is set - if (options.json) { - outputContent = options.pretty - ? JSON.stringify({ success: true, data: result.data }, null, 2) - : JSON.stringify({ success: true, data: result.data }); - } else { - // Default to human-readable format (one URL per line) - outputContent = formatMapReadable(result.data); - } + const outputContent = formatMapReadable(result.data); writeOutput(outputContent, options.output, !!options.output); } diff --git a/src/commands/parse.ts b/src/commands/parse.ts index 4aa832fd9f..c77618ecff 100644 --- a/src/commands/parse.ts +++ b/src/commands/parse.ts @@ -13,7 +13,13 @@ import type { ParseOptions, ParseResult } from '../types/parse'; import type { ScrapeFormat } from '../types/scrape'; import { getClient, isKeylessMode } from '../utils/client'; import { getConfig, validateConfig } from '../utils/config'; -import { handleScrapeOutput } from '../utils/output'; +import { handleScrapeOutput, shouldOutputJson } from '../utils/output'; +import { + agentHintMetadata, + withoutAgentHints, + writeAgentHints, +} from '../utils/agent-hints'; +import { apiFailure } from './alexandria'; const DEFAULT_API_URL = 'https://api.firecrawl.dev'; @@ -198,12 +204,17 @@ export async function executeParse( const message = payload?.error || `HTTP ${response.status}: ${response.statusText || 'Request failed'}`; - return { success: false, error: message }; + return { + ...apiFailure({ response: { data: payload } }), + success: false, + error: message, + }; } return { success: true, - data: payload?.data ?? payload, + data: withoutAgentHints(payload?.data ?? payload), + ...agentHintMetadata(payload), }; } catch (error) { const requestEndTime = Date.now(); @@ -220,10 +231,18 @@ export async function executeParse( * /v2/parse response shape matches /v2/scrape. */ export async function handleParseCommand(options: ParseOptions): Promise { - const result = await executeParse(options); - - if (options.query && result.success && result.data?.answer) { + const response = await executeParse(options); + const result = + options.agentHints === false ? withoutAgentHints(response) : response; + + if ( + options.query && + result.success && + result.data?.answer && + !shouldOutputJson(options.output, options.json) + ) { const { writeOutput } = await import('../utils/output'); + writeAgentHints(result); writeOutput(result.data.answer, options.output, !!options.output); return; } diff --git a/src/commands/scrape.ts b/src/commands/scrape.ts index 6b693935b2..f2b31aed33 100644 --- a/src/commands/scrape.ts +++ b/src/commands/scrape.ts @@ -10,7 +10,16 @@ import type { ScrapeLocation, } from '../types/scrape'; import { getClient, isKeylessMode, keylessRequest } from '../utils/client'; -import { handleScrapeOutput, writeOutput } from '../utils/output'; +import { + handleScrapeOutput, + writeOutput, + shouldOutputJson, +} from '../utils/output'; +import { + agentHintMetadata, + withoutAgentHints, + writeAgentHints, +} from '../utils/agent-hints'; import { saveInteractSession, clearInteractSession, @@ -18,7 +27,7 @@ import { import { getOrigin } from '../utils/url'; import { executeMap } from './map'; import { getStatus } from './status'; -import { requireAlexandriaKey } from './alexandria'; +import { requireAlexandriaKey, apiFailure } from './alexandria'; /** * Output timing information if requested @@ -148,6 +157,7 @@ export async function executeScrape( scrapeParams.domainTools = true; } let result: any; + let hints = {}; if (isKeylessMode(options.apiKey, options.apiUrl)) { // Keyless free tier: header-less request. The API identifies the CLI via // the `integration: 'cli'` field already in scrapeParams. @@ -155,13 +165,18 @@ export async function executeScrape( url: options.url, ...scrapeParams, }); + if (json?.success === false) { + return apiFailure({ response: { data: json } }); + } result = json?.data ?? json; + hints = agentHintMetadata(json); } else { const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl, }); result = await app.scrape(options.url, scrapeParams); + hints = agentHintMetadata(result); } const requestEndTime = Date.now(); outputTiming(options, requestStartTime, requestEndTime); @@ -185,16 +200,14 @@ export async function executeScrape( return { success: true, - data: result, + data: withoutAgentHints(result), + ...hints, }; } catch (error) { const requestEndTime = Date.now(); outputTiming(options, requestStartTime, requestEndTime, error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - }; + return apiFailure(error, 'Unknown error occurred'); } } @@ -204,10 +217,18 @@ export async function executeScrape( export async function handleScrapeCommand( options: ScrapeOptions ): Promise { - const result = await executeScrape(options); + const response = await executeScrape(options); + const result = + options.agentHints === false ? withoutAgentHints(response) : response; // Query mode: output answer directly - if (options.query && result.success && result.data?.answer) { + if ( + options.query && + result.success && + result.data?.answer && + !shouldOutputJson(options.output, options.json) + ) { + writeAgentHints(result); writeOutput(result.data.answer, options.output, !!options.output); return; } @@ -274,6 +295,7 @@ export async function handleMultiScrapeCommand( const promises = urls.map(async (url) => { const scrapeOptions: ScrapeOptions = { ...options, url }; const result = await executeScrape(scrapeOptions); + writeAgentHints(result, options.agentHints); const currentCount = ++completedCount; diff --git a/src/commands/search.ts b/src/commands/search.ts index 4181ed37a0..f8064fc220 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -15,6 +15,11 @@ import type { import { getClient, isKeylessMode, keylessRequest } from '../utils/client'; import { writeOutput } from '../utils/output'; import { apiFailure, requireAlexandriaKey } from './alexandria'; +import { + agentHintMetadata, + writeAgentHints, + withoutAgentHints, +} from '../utils/agent-hints'; /** * Execute search command @@ -123,6 +128,10 @@ export async function executeSearch( ); envelope = (httpResponse?.data ?? {}) as Record; } + if (envelope.success === false) + return apiFailure({ + response: { data: envelope }, + }); const payload = (envelope.data ?? {}) as Record; const data: SearchResultData = {}; @@ -141,17 +150,10 @@ export async function executeSearch( warning: envelope.warning, id: envelope.id, creditsUsed: envelope.creditsUsed, + ...agentHintMetadata(envelope), }; } catch (error) { - return { - success: false, - error: - options.domainTools || options.sources?.includes('alexandria') - ? JSON.stringify(apiFailure(error)) - : error instanceof Error - ? error.message - : 'Unknown error occurred', - }; + return apiFailure(error, 'Unknown error occurred'); } } @@ -293,7 +295,21 @@ function formatSearchReadable( export async function handleSearchCommand( options: SearchOptions ): Promise { - const result = await executeSearch(options); + const response = await executeSearch(options); + const result = + options.agentHints === false ? withoutAgentHints(response) : response; + const json = options.json || options.pretty; + + if (json) { + writeOutput( + JSON.stringify(result, null, options.pretty ? 2 : undefined), + options.output, + !!options.output + ); + if (!result.success) process.exitCode = 1; + return; + } + writeAgentHints(result); if (!result.success) { console.error('Error:', result.error); @@ -312,38 +328,12 @@ export async function handleSearchCommand( (result.data.news && result.data.news.length > 0) || (result.data.developer && result.data.developer.length > 0); - if (!hasResults && !(result.data.tools && (options.json || options.pretty))) { + if (!hasResults) { console.log('No results found.'); return; } - let outputContent: string; - - // Use JSON format if --json or --pretty flag is set - // --pretty implies JSON output - if (options.json || options.pretty) { - const jsonOutput: Record = { - success: true, - data: result.data, - }; - - if (result.warning) { - jsonOutput.warning = result.warning; - } - if (result.id) { - jsonOutput.id = result.id; - } - if (result.creditsUsed !== undefined) { - jsonOutput.creditsUsed = result.creditsUsed; - } - - outputContent = options.pretty - ? JSON.stringify(jsonOutput, null, 2) - : JSON.stringify(jsonOutput); - } else { - // Default to human-readable format - outputContent = formatSearchReadable(result.data, options); - } + const outputContent = formatSearchReadable(result.data, options); writeOutput(outputContent, options.output, !!options.output); } diff --git a/src/index.ts b/src/index.ts index 9132719df9..7c056168d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -382,6 +382,7 @@ function createScrapeCommand(): Command { ) .option('--api-url ', 'API URL (overrides global --api-url)') .option('-o, --output ', 'Output file path (default: stdout)') + .option('--no-agent-hints', 'Omit server guidance from CLI output') .option('--json', 'Output as JSON format', false) .option('--pretty', 'Pretty print JSON output', false) .option( @@ -801,6 +802,7 @@ function createMapCommand(): Command { ) .option('--api-url ', 'API URL (overrides global --api-url)') .option('-o, --output ', 'Output file path (default: stdout)') + .option('--no-agent-hints', 'Omit server guidance from CLI output') .option('--json', 'Output as JSON format', false) .option('--pretty', 'Pretty print JSON output', false) .action(async (positionalUrl, options) => { @@ -818,6 +820,7 @@ function createMapCommand(): Command { wait: options.wait, output: options.output, json: options.json, + agentHints: options.agentHints, pretty: options.pretty, apiKey: options.apiKey, apiUrl: options.apiUrl, @@ -868,6 +871,7 @@ function createParseCommand(): Command { ) .option('--api-url ', 'API URL (overrides global --api-url)') .option('-o, --output ', 'Output file path (default: stdout)') + .option('--no-agent-hints', 'Omit server guidance from CLI output') .option('--json', 'Output as JSON format', false) .option('--pretty', 'Pretty print JSON output', false) .option( @@ -918,6 +922,7 @@ Max upload size: 50 MB output: options.output, pretty: options.pretty, json: options.json, + agentHints: options.agentHints, timing: options.timing, query: options.query, }); @@ -992,6 +997,7 @@ function createSearchCommand(): Command { ) .option('--api-url ', 'API URL (overrides global --api-url)') .option('-o, --output ', 'Output file path (default: stdout)') + .option('--no-agent-hints', 'Omit server guidance from CLI output') // .option( // '-p, --pretty', // 'Output as pretty JSON (default: human-readable)', @@ -1064,6 +1070,7 @@ function createSearchCommand(): Command { apiUrl: options.apiUrl, output: options.output, json: options.json, + agentHints: options.agentHints, pretty: options.pretty, }; diff --git a/src/types/map.ts b/src/types/map.ts index 07bf3e8fc6..6d9aa6dce0 100644 --- a/src/types/map.ts +++ b/src/types/map.ts @@ -2,7 +2,9 @@ * Types for map command */ -export interface MapOptions { +import type { AgentHintMetadata, AgentHintOptions } from '../utils/agent-hints'; + +export interface MapOptions extends AgentHintOptions { /** API key for Firecrawl */ apiKey?: string; /** API URL for Firecrawl */ @@ -33,8 +35,9 @@ export interface MapOptions { timeout?: number; } -export interface MapResult { +export interface MapResult extends AgentHintMetadata { success: boolean; + id?: string; data?: { links: Array<{ url: string; diff --git a/src/types/parse.ts b/src/types/parse.ts index 7499ef541b..945f703da2 100644 --- a/src/types/parse.ts +++ b/src/types/parse.ts @@ -3,8 +3,9 @@ */ import type { ScrapeFormat, ScrapeLocation } from './scrape'; +import type { AgentHintMetadata, AgentHintOptions } from '../utils/agent-hints'; -export interface ParseOptions { +export interface ParseOptions extends AgentHintOptions { /** Local file path to parse */ file: string; /** Output format(s) */ @@ -35,7 +36,7 @@ export interface ParseOptions { query?: string; } -export interface ParseResult { +export interface ParseResult extends AgentHintMetadata { success: boolean; data?: any; error?: string; diff --git a/src/types/scrape.ts b/src/types/scrape.ts index ee323751f1..7f0918c4c5 100644 --- a/src/types/scrape.ts +++ b/src/types/scrape.ts @@ -2,6 +2,8 @@ * Types and interfaces for the scrape command */ +import type { AgentHintMetadata, AgentHintOptions } from '../utils/agent-hints'; + export type ScrapeFormat = | 'markdown' | 'html' @@ -22,7 +24,7 @@ export interface ScrapeLocation { languages?: string[]; } -export interface ScrapeOptions { +export interface ScrapeOptions extends AgentHintOptions { domainTools?: boolean; /** URL to scrape */ url: string; @@ -75,7 +77,7 @@ export interface ScrapeOptions { redactPII?: boolean; } -export interface ScrapeResult { +export interface ScrapeResult extends AgentHintMetadata { success: boolean; data?: any; error?: string; diff --git a/src/types/search.ts b/src/types/search.ts index 8378a60ec5..f35bcf2462 100644 --- a/src/types/search.ts +++ b/src/types/search.ts @@ -3,11 +3,12 @@ */ import type { ScrapeFormat } from './scrape'; +import type { AgentHintMetadata, AgentHintOptions } from '../utils/agent-hints'; export type SearchSource = 'web' | 'images' | 'news' | 'alexandria'; export type SearchCategory = 'github' | 'research' | 'pdf' | 'developer'; -export interface SearchOptions { +export interface SearchOptions extends AgentHintOptions { domainTools?: boolean; /** Search query (required) */ query: string; @@ -121,7 +122,7 @@ export interface SearchResultData { developer?: DeveloperSearchResult[]; } -export interface SearchResult { +export interface SearchResult extends AgentHintMetadata { success: boolean; data?: SearchResultData; warning?: string; diff --git a/src/utils/agent-hints.ts b/src/utils/agent-hints.ts new file mode 100644 index 0000000000..e1f98b5809 --- /dev/null +++ b/src/utils/agent-hints.ts @@ -0,0 +1,50 @@ +/** Server-authored guidance. The CLI displays it; it never executes it. */ +export interface AgentHintOptions { + /** Suppress returned guidance locally, including in JSON output. */ + agentHints?: boolean; +} + +export interface AgentHintMetadata { + agent_hints?: string[]; +} + +/** Only accept the documented response field; never derive hints from content. */ +export function agentHintMetadata( + source: unknown, + enabled = true +): AgentHintMetadata { + if (!enabled || !source || typeof source !== 'object') return {}; + const hints = (source as AgentHintMetadata).agent_hints; + if (!Array.isArray(hints)) return {}; + return { + agent_hints: hints.filter((hint) => typeof hint === 'string').slice(0, 3), + }; +} + +/** SDK convenience methods may place response hints beside document fields. */ +export function withoutAgentHints(value: T): T { + if (!value || typeof value !== 'object' || !('agent_hints' in value)) { + return value; + } + const { agent_hints: _hints, ...rest } = value; + return rest as T; +} + +export function errorAgentHints(error: unknown): AgentHintMetadata { + const value = error as any; + return { + ...agentHintMetadata(value?.details), + ...agentHintMetadata(value?.response?.data), + ...agentHintMetadata(error), + }; +} + +/** stderr keeps raw page content, pipes, and output files free of guidance. */ +export function writeAgentHints(source: unknown, enabled = true): void { + const hints = agentHintMetadata(source, enabled).agent_hints; + if (hints?.length) { + process.stderr.write( + `Agent hints:\n${hints.map((hint) => `- ${hint}`).join('\n')}\n` + ); + } +} diff --git a/src/utils/client.ts b/src/utils/client.ts index 6519495bfc..84301fe2d0 100644 --- a/src/utils/client.ts +++ b/src/utils/client.ts @@ -40,9 +40,13 @@ export async function keylessRequest( body: JSON.stringify(body), }); const json: any = await response.json().catch(() => ({})); - if (!response.ok) { - throw new Error( - json?.error || `Firecrawl request failed (HTTP ${response.status})` + if (!response.ok || json?.success === false) { + // Keep the API envelope so command renderers can preserve typed errors and hints. + throw Object.assign( + new Error( + json?.error || `Firecrawl request failed (HTTP ${response.status})` + ), + { response: { data: json } } ); } return json; diff --git a/src/utils/options.ts b/src/utils/options.ts index 9d53d61bb0..1f014e199b 100644 --- a/src/utils/options.ts +++ b/src/utils/options.ts @@ -117,6 +117,7 @@ export function parseScrapeOptions(options: any): ScrapeOptions { output: options.output, pretty: options.pretty, json: options.json, + agentHints: options.agentHints, timing: options.timing, maxAge: options.maxAge, location, diff --git a/src/utils/output.ts b/src/utils/output.ts index 273e6bc51c..c3b398edce 100644 --- a/src/utils/output.ts +++ b/src/utils/output.ts @@ -5,11 +5,15 @@ import * as fs from 'fs'; import * as path from 'path'; import type { ScrapeResult, ScrapeFormat } from '../types/scrape'; +import { agentHintMetadata, writeAgentHints } from './agent-hints'; /** * Determine if output should be JSON based on flag or file extension */ -function shouldOutputJson(outputPath?: string, jsonFlag?: boolean): boolean { +export function shouldOutputJson( + outputPath?: string, + jsonFlag?: boolean +): boolean { // Explicit --json flag takes precedence if (jsonFlag) return true; @@ -177,28 +181,51 @@ export function handleScrapeOutput( pretty: boolean = false, json: boolean = false ): void { + const explicitJson = shouldOutputJson(outputPath, json); if (!result.success) { + if (explicitJson) { + writeOutput( + JSON.stringify(result, null, pretty ? 2 : undefined), + outputPath, + !!outputPath + ); + process.exitCode = 1; + return; + } + writeAgentHints(result); // Always use stderr for errors to allow piping console.error('Error:', result.error); process.exit(1); + return; } if (!result.data) { + if (explicitJson) { + writeOutput( + JSON.stringify(result, null, pretty ? 2 : undefined), + outputPath, + !!outputPath + ); + } else { + writeAgentHints(result); + } return; } // Determine if we should force JSON output - const forceJson = - shouldOutputJson(outputPath, json) || - Array.isArray((result.data as any).tools); + const forceJson = explicitJson || Array.isArray((result.data as any).tools); // If JSON is forced, always output JSON regardless of format if (forceJson) { let jsonContent: string; try { jsonContent = pretty - ? JSON.stringify(result.data, null, 2) - : JSON.stringify(result.data); + ? JSON.stringify( + { ...result.data, ...agentHintMetadata(result) }, + null, + 2 + ) + : JSON.stringify({ ...result.data, ...agentHintMetadata(result) }); } catch (error) { jsonContent = JSON.stringify({ error: 'Failed to serialize response', @@ -219,6 +246,7 @@ export function handleScrapeOutput( if (isSingleFormat && isRawTextFormat && singleFormat) { const content = extractContent(result.data, singleFormat); if (content !== null) { + writeAgentHints(result); writeOutput(content, outputPath, !!outputPath); return; } @@ -231,6 +259,7 @@ export function handleScrapeOutput( result.data.screenshot ) { const content = formatScreenshotOutput(result.data); + writeAgentHints(result); writeOutput(content, outputPath, !!outputPath); return; } @@ -245,6 +274,7 @@ export function handleScrapeOutput( // Multiple formats - extract only requested formats outputData = extractMultipleFormats(result.data, formats); } + outputData = { ...outputData, ...agentHintMetadata(result) }; let jsonContent: string; try { From ccd300cc019beaefd5193785b25c6ad32c0ca8f4 Mon Sep 17 00:00:00 2001 From: Rakshith Ramprakash Date: Thu, 17 Sep 2026 16:34:18 +1000 Subject: [PATCH 2/3] fix(cli): opt in to API agent hints --- README.md | 2 + src/__tests__/commands/parse.test.ts | 5 +- .../utils/client-agent-hints.test.ts | 66 +++++++++++++++++++ src/commands/parse.ts | 8 ++- src/utils/client.ts | 37 +++++++++-- 5 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 src/__tests__/utils/client-agent-hints.test.ts diff --git a/README.md b/README.md index 2d164f8802..533cfd552f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Command-line interface for Firecrawl. Search, scrape, interact, crawl, map, sear Search, Scrape, Parse, Map, and Alexandria commands preserve optional `agent_hints` returned by the API. These are up to three server-authored suggestions for useful next requests or feedback after evaluating a result. The CLI does not generate guidance, execute suggested requests, or submit feedback automatically. +The CLI explicitly opts into this guidance with the `X-Firecrawl-Agent-Hints: true` request header. The API leaves hints disabled by default for ordinary HTTP and SDK callers. + - JSON output includes `agent_hints` when returned, including empty results and errors. Scrape and Parse retain their existing document-shaped JSON, with hints alongside document fields. - Readable results and raw page output show hints on stderr, so pipes and saved markdown/HTML stay clean. Capture stderr as well as stdout when an agent uses readable output. - Pass `--no-agent-hints` to omit hints from CLI output, including JSON. This is local suppression; it does not change server-side feedback preferences or API behavior. diff --git a/src/__tests__/commands/parse.test.ts b/src/__tests__/commands/parse.test.ts index 18b126d664..bce39ff94a 100644 --- a/src/__tests__/commands/parse.test.ts +++ b/src/__tests__/commands/parse.test.ts @@ -61,7 +61,9 @@ describe('executeParse', () => { ]; expect(url).toBe('https://api.firecrawl.dev/v2/parse'); expect(init.method).toBe('POST'); - expect(init.headers).toBeUndefined(); + expect(init.headers).toEqual({ + 'X-Firecrawl-Agent-Hints': 'true', + }); const options = JSON.parse(init.body.get('options') as string); expect(options).toEqual({ @@ -84,6 +86,7 @@ describe('executeParse', () => { { headers?: Record }, ]; expect(init.headers).toEqual({ + 'X-Firecrawl-Agent-Hints': 'true', Authorization: 'Bearer fc-test-key', }); }); diff --git a/src/__tests__/utils/client-agent-hints.test.ts b/src/__tests__/utils/client-agent-hints.test.ts new file mode 100644 index 0000000000..a5e1a817cd --- /dev/null +++ b/src/__tests__/utils/client-agent-hints.test.ts @@ -0,0 +1,66 @@ +import { createServer, type Server } from 'node:http'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { getClient, keylessRequest } from '../../utils/client'; +import { initializeConfig } from '../../utils/config'; +import { setupTest, teardownTest } from './mock-client'; + +vi.mock('../../utils/credentials', () => ({ + loadCredentials: vi.fn(() => null), +})); + +describe('agent hints request opt-in', () => { + let server: Server; + let apiUrl: string; + let requestHeaders: Array>; + + beforeEach(async () => { + setupTest(); + requestHeaders = []; + server = createServer((request, response) => { + requestHeaders.push(request.headers); + request.resume(); + request.on('end', () => { + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify({ success: true })); + }); + }); + + await new Promise((resolve) => { + server.listen(0, '127.0.0.1', resolve); + }); + const address = server.address(); + if (!address || typeof address === 'string') { + throw new Error('Test server did not bind to a TCP port'); + } + apiUrl = `http://127.0.0.1:${address.port}`; + }); + + afterEach(async () => { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + teardownTest(); + }); + + it('opts in on authenticated SDK requests', async () => { + const client = getClient({ + apiKey: 'fc-test', + apiUrl, + maxRetries: 1, + }); + + await (client as any).http.post('/v2/search', { query: 'test' }); + + expect(requestHeaders).toHaveLength(1); + expect(requestHeaders[0]['x-firecrawl-agent-hints']).toBe('true'); + }); + + it('opts in on keyless requests', async () => { + initializeConfig({ apiUrl }); + + await keylessRequest('/v2/search', { query: 'test' }); + + expect(requestHeaders).toHaveLength(1); + expect(requestHeaders[0]['x-firecrawl-agent-hints']).toBe('true'); + }); +}); diff --git a/src/commands/parse.ts b/src/commands/parse.ts index c77618ecff..2e3485ee00 100644 --- a/src/commands/parse.ts +++ b/src/commands/parse.ts @@ -11,7 +11,7 @@ import * as path from 'path'; import type { FormatOption } from 'firecrawl'; import type { ParseOptions, ParseResult } from '../types/parse'; import type { ScrapeFormat } from '../types/scrape'; -import { getClient, isKeylessMode } from '../utils/client'; +import { AGENT_HINTS_HEADERS, getClient, isKeylessMode } from '../utils/client'; import { getConfig, validateConfig } from '../utils/config'; import { handleScrapeOutput, shouldOutputJson } from '../utils/output'; import { @@ -190,8 +190,10 @@ export async function executeParse( try { const response = await fetch(`${apiUrl}/v2/parse`, { method: 'POST', - headers: - !keyless && apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined, + headers: { + ...AGENT_HINTS_HEADERS, + ...(!keyless && apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), + }, body: form, }); diff --git a/src/utils/client.ts b/src/utils/client.ts index 84301fe2d0..35edeb0a7a 100644 --- a/src/utils/client.ts +++ b/src/utils/client.ts @@ -18,6 +18,29 @@ let clientInstance: Firecrawl | null = null; const DEFAULT_API_URL = 'https://api.firecrawl.dev'; +export const AGENT_HINTS_HEADERS = { + 'X-Firecrawl-Agent-Hints': 'true', +} as const; + +function createClient(options: FirecrawlClientOptions): Firecrawl { + const client = new Firecrawl(options); + const http = (client as any).http?.instance; + + if (!http?.interceptors?.request?.use) { + throw new Error('Firecrawl SDK client cannot enable API agent hints'); + } + + http.interceptors.request.use((request: any) => { + request.headers = { + ...request.headers, + ...AGENT_HINTS_HEADERS, + }; + return request; + }); + + return client; +} + /** * Keyless free tier: scrape and search work without an API key against the * Firecrawl cloud (rate-limited per IP). The cloud only grants this when NO @@ -36,7 +59,10 @@ export async function keylessRequest( const apiUrl = (getConfig().apiUrl || DEFAULT_API_URL).replace(/\/$/, ''); const response = await fetch(`${apiUrl}${path}`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + ...AGENT_HINTS_HEADERS, + }, body: JSON.stringify(body), }); const json: any = await response.json().catch(() => ({})); @@ -56,7 +82,10 @@ export async function keylessGet(path: string): Promise { const apiUrl = (getConfig().apiUrl || DEFAULT_API_URL).replace(/\/$/, ''); const response = await fetch(`${apiUrl}${path}`, { method: 'GET', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + ...AGENT_HINTS_HEADERS, + }, }); const json: any = await response.json().catch(() => ({})); if (!response.ok) { @@ -121,7 +150,7 @@ export function getClient( backoffFactor: options.backoffFactor ?? config.backoffFactor, }; - return new Firecrawl(clientOptions); + return createClient(clientOptions); } // Return singleton instance or create one @@ -137,7 +166,7 @@ export function getClient( backoffFactor: config.backoffFactor, }; - clientInstance = new Firecrawl(clientOptions); + clientInstance = createClient(clientOptions); } return clientInstance; From 7a2922f76eaf604b493ffd9afa64a8a50d792a76 Mon Sep 17 00:00:00 2001 From: Rakshith Ramprakash Date: Fri, 18 Sep 2026 03:54:23 +1000 Subject: [PATCH 3/3] refactor(cli): keep feedback out of response hints --- README.md | 2 +- src/__tests__/alexandria-beta.test.ts | 4 +++- src/__tests__/commands/agent-hints.test.ts | 6 +++--- src/utils/agent-hints.ts | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 76d73c700a..5a205d68a2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Command-line interface for Firecrawl. Search, scrape, interact, crawl, map, sear ### API response guidance -Search, Scrape, Parse, Map, and Alexandria commands preserve optional `agent_hints` returned by the API. These are up to three server-authored suggestions for useful next requests or feedback after evaluating a result. The CLI does not generate guidance, execute suggested requests, or submit feedback automatically. +Search, Scrape, Parse, Map, and Alexandria commands preserve optional `agent_hints` returned by the API. These are up to two server-authored suggestions for useful next requests. Static feedback instructions are not response hints. The CLI does not generate guidance, execute suggested requests, or submit feedback automatically. The CLI explicitly opts into this guidance with the `X-Firecrawl-Agent-Hints: true` request header. The API leaves hints disabled by default for ordinary HTTP and SDK callers. diff --git a/src/__tests__/alexandria-beta.test.ts b/src/__tests__/alexandria-beta.test.ts index dbbf9ac443..b89f2a1ab9 100644 --- a/src/__tests__/alexandria-beta.test.ts +++ b/src/__tests__/alexandria-beta.test.ts @@ -419,7 +419,9 @@ it('preserves search hints on empty JSON responses and supports local suppressio success: true, id: 'search-empty', data: { web: [] }, - agent_hints: ['After evaluation, submit feedback for search-empty.'], + agent_hints: [ + 'Try a narrower query if the result set does not cover the task.', + ], }; const args = ['search', 'example', '--sources', 'web', '--json']; const result = await cli(args); diff --git a/src/__tests__/commands/agent-hints.test.ts b/src/__tests__/commands/agent-hints.test.ts index 5a1f6dba1a..c1dfdd4e12 100644 --- a/src/__tests__/commands/agent-hints.test.ts +++ b/src/__tests__/commands/agent-hints.test.ts @@ -21,7 +21,7 @@ vi.mock('../../utils/client', async () => ({ describe('server agent hints', () => { const hints = [ 'Inspect the returned tool definition.', - 'Submit feedback after evaluation.', + 'Use the selected tool definition to make the next request.', ]; let directory: string; let post: ReturnType; @@ -283,10 +283,10 @@ describe('server agent hints', () => { expect(process.exitCode).toBe(1); }); - it('accepts only string hints and caps malformed upstream arrays at three', () => { + it('accepts only string hints and caps malformed upstream arrays at two', () => { expect( agentHintMetadata({ agent_hints: ['one', null, 'two', 'three', 'four'] }) - ).toEqual({ agent_hints: ['one', 'two', 'three'] }); + ).toEqual({ agent_hints: ['one', 'two'] }); expect(agentHintMetadata({ agent_hints: 'not an array' })).toEqual({}); expect(agentHintMetadata({})).toEqual({}); }); diff --git a/src/utils/agent-hints.ts b/src/utils/agent-hints.ts index e1f98b5809..d179dc46d0 100644 --- a/src/utils/agent-hints.ts +++ b/src/utils/agent-hints.ts @@ -17,7 +17,7 @@ export function agentHintMetadata( const hints = (source as AgentHintMetadata).agent_hints; if (!Array.isArray(hints)) return {}; return { - agent_hints: hints.filter((hint) => typeof hint === 'string').slice(0, 3), + agent_hints: hints.filter((hint) => typeof hint === 'string').slice(0, 2), }; }