From f201fe9d37e8695aef9353a731ef6b2e8b571da4 Mon Sep 17 00:00:00 2001 From: mogery Date: Tue, 6 Oct 2026 21:47:25 +0000 Subject: [PATCH 1/2] fix(agent): validate Alexandria flags locally and fix the exchange docs Follow-up to #305's review: - Reject --max-calls values that are not whole numbers from 1 to 30, --toolkits with more than 5 slugs, and --call-ids/--always without --approve, before calling the API. - Terms approvals: name the provider and point at `firecrawl alexandria terms accept` as well as the dashboard. - README: document --thread and --mode in the agent options table. - SKILL.md: include the prompt in the follow-up command and describe both ways to accept terms. Release 1.26.3. --- README.md | 2 ++ package.json | 2 +- skills/firecrawl-agent/SKILL.md | 2 +- src/__tests__/alexandria-beta.test.ts | 46 +++++++++++++++++++++++++-- src/commands/agent.ts | 4 +-- src/index.ts | 30 ++++++++++++++--- 6 files changed, 76 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f7d2924215..0aceff96c4 100644 --- a/README.md +++ b/README.md @@ -704,6 +704,8 @@ firecrawl agent --wait | `--schema-file ` | Path to JSON schema file for structured output | | `--max-credits ` | Maximum credits to spend (job fails if exceeded) | | `--webhook ` | Webhook URL or configuration | +| `--thread ` | Continue an existing thread with this prompt as the next turn | +| `--mode ` | `extract` returns structured data; `chat` returns a text message | | `--alexandria` | Let the agent call Alexandria providers (implied by the flags below); `--no-alexandria` keeps it off them | | `--toolkits ` | Comma-separated provider slugs the agent may use (up to 5; default: the whole catalog) | | `--max-calls ` | Most provider calls the agent may make this turn (1-30) | diff --git a/package.json b/package.json index 5c7213efeb..9e68024862 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firecrawl-cli", - "version": "1.26.2", + "version": "1.26.3", "publishConfig": { "tag": "latest" }, diff --git a/skills/firecrawl-agent/SKILL.md b/skills/firecrawl-agent/SKILL.md index fffc42f0bd..1d8feeb62e 100644 --- a/skills/firecrawl-agent/SKILL.md +++ b/skills/firecrawl-agent/SKILL.md @@ -37,7 +37,7 @@ A run uses connected Alexandria data providers only when it starts with an Alexa firecrawl agent "find the head of sales at " --toolkits apollo,crunchbase --wait --json -o .firecrawl/contacts.json ``` -With `--require-approval` (needs `--mode chat`), a run can end on a `pendingApproval` instead of making a paid call. Ask the user, then answer it on the same thread with `--thread --mode chat --approve ` (or `--decline `). A `terms` approval only continues after an organization admin has accepted the provider's terms in the Firecrawl dashboard; approving does not accept them. +With `--require-approval` (needs `--mode chat`), a run can end on a `pendingApproval` instead of making a paid call. Ask the user, then answer it on the same thread with `firecrawl agent "" --thread --mode chat --approve ` (or `--decline `). A `terms` approval only continues once the provider's terms are accepted for the organization, either in the dashboard or by showing the user `firecrawl alexandria terms show ` and, only after they explicitly agree, running `firecrawl alexandria terms accept`. Approving does not accept them. ## Job IDs diff --git a/src/__tests__/alexandria-beta.test.ts b/src/__tests__/alexandria-beta.test.ts index abb375c056..00f24e6adc 100644 --- a/src/__tests__/alexandria-beta.test.ts +++ b/src/__tests__/alexandria-beta.test.ts @@ -1138,7 +1138,7 @@ it('sends no exchange without an Alexandria flag', async () => { expect(requests[0].body).not.toHaveProperty('exchange'); }); -it('rejects approval flags the API cannot honor before calling it', async () => { +it('rejects Alexandria flags the API cannot honor before calling it', async () => { const both = await cli([ 'agent', 'Go ahead.', @@ -1152,6 +1152,21 @@ it('rejects approval flags the API cannot honor before calling it', async () => expect(both.code).toBe(1); expect(both.stderr).toContain('use --approve or --decline, not both'); + const declineWithCallIds = await cli([ + 'agent', + 'Never mind.', + '--thread', + THREAD_ID, + '--decline', + APPROVAL_ID, + '--call-ids', + 'call-1', + ]); + expect(declineWithCallIds.code).toBe(1); + expect(declineWithCallIds.stderr).toContain( + '--call-ids and --always only apply with --approve' + ); + const noThread = await cli(['agent', 'Go ahead.', '--decline', APPROVAL_ID]); expect(noThread.code).toBe(1); expect(noThread.stderr).toContain('pass that thread with --thread'); @@ -1159,6 +1174,30 @@ it('rejects approval flags the API cannot honor before calling it', async () => const noChat = await cli(['agent', 'Find contacts.', '--require-approval']); expect(noChat.code).toBe(1); expect(noChat.stderr).toContain('--require-approval needs --mode chat'); + + for (const value of ['12o', '0', '31', '2.5']) { + const maxCalls = await cli([ + 'agent', + 'Find contacts.', + '--max-calls', + value, + ]); + expect(maxCalls.code).toBe(1); + expect(maxCalls.stderr).toContain( + '--max-calls must be a whole number from 1 to 30' + ); + } + + const toolkits = await cli([ + 'agent', + 'Find contacts.', + '--toolkits', + 'a,b,c,d,e,f', + ]); + expect(toolkits.code).toBe(1); + expect(toolkits.stderr).toContain( + '--toolkits takes at most 5 provider slugs' + ); expect(requests).toHaveLength(0); }); @@ -1266,7 +1305,10 @@ it('shows a pending approval and how to answer it', async () => { }; responseFor = undefined; const terms = await cli(['agent', RUN_ID]); - expect(terms.stdout).toContain('accept them in the Firecrawl dashboard'); + expect(terms.stdout).toContain('Approving does not accept terms.'); + expect(terms.stdout).toContain( + ' - Crunchbase (crunchbase): https://example.com/terms/crunchbase' + ); expect(terms.stdout).toContain(`--approve ${APPROVAL_ID}`); }); diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 81f4aca5af..faa6d0ba7a 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -523,10 +523,10 @@ function pendingApprovalLines( const lines = [`Pending approval ${approval.id}: ${approval.reason}`]; if (approval.kind === 'terms') { lines.push( - 'Approving does not accept terms; accept them in the Firecrawl dashboard first:' + 'Approving does not accept terms. Accept them first with firecrawl alexandria terms accept , or in the dashboard:' ); for (const gate of approval.terms) { - lines.push(` - ${gate.name}: ${gate.url}`); + lines.push(` - ${gate.name} (${gate.provider}): ${gate.url}`); } } else { for (const call of approval.calls) { diff --git a/src/index.ts b/src/index.ts index 1c8c0835c5..331e612cc0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1710,8 +1710,7 @@ function createAgentCommand(): Command { ) .option( '--max-calls ', - 'Most provider calls the agent may make this turn (1-30)', - parseInt + 'Most provider calls the agent may make this turn (1-30)' ) .option( '--require-approval', @@ -1767,6 +1766,12 @@ function createAgentCommand(): Command { ); process.exit(1); } + if ((options.callIds || options.always) && !options.approve) { + console.error( + 'Error: --call-ids and --always only apply with --approve.' + ); + process.exit(1); + } if ((options.approve || options.decline) && !options.thread) { console.error( "Error: --approve and --decline answer a thread's pending approval; pass that thread with --thread." @@ -1826,10 +1831,27 @@ function createAgentCommand(): Command { process.exit(1); } + const toolkits = parseCommaList(options.toolkits); + if (toolkits && toolkits.length > 5) { + console.error('Error: --toolkits takes at most 5 provider slugs.'); + process.exit(1); + } + const maxCalls = + options.maxCalls === undefined ? undefined : Number(options.maxCalls); + if ( + maxCalls !== undefined && + !(Number.isInteger(maxCalls) && maxCalls >= 1 && maxCalls <= 30) + ) { + console.error( + 'Error: --max-calls must be a whole number from 1 to 30.' + ); + process.exit(1); + } + const exchange: AgentExchangeOptions = { enabled: options.alexandria, - toolkits: parseCommaList(options.toolkits), - maxCalls: options.maxCalls, + toolkits, + maxCalls, requireApproval: options.requireApproval, approve: options.approve && { approvalId: options.approve, From 448aa83af6cbf596da3e69ee3c8853afa936110e Mon Sep 17 00:00:00 2001 From: mogery Date: Tue, 6 Oct 2026 21:52:50 +0000 Subject: [PATCH 2/2] fix(agent): catch empty --call-ids, test --always, and spell out terms acceptance --- skills/firecrawl-agent/SKILL.md | 2 +- src/__tests__/alexandria-beta.test.ts | 18 +++++++++++++++--- src/commands/agent.ts | 7 +++++-- src/index.ts | 5 ++++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/skills/firecrawl-agent/SKILL.md b/skills/firecrawl-agent/SKILL.md index 1d8feeb62e..7ef7e5610f 100644 --- a/skills/firecrawl-agent/SKILL.md +++ b/skills/firecrawl-agent/SKILL.md @@ -37,7 +37,7 @@ A run uses connected Alexandria data providers only when it starts with an Alexa firecrawl agent "find the head of sales at " --toolkits apollo,crunchbase --wait --json -o .firecrawl/contacts.json ``` -With `--require-approval` (needs `--mode chat`), a run can end on a `pendingApproval` instead of making a paid call. Ask the user, then answer it on the same thread with `firecrawl agent "" --thread --mode chat --approve ` (or `--decline `). A `terms` approval only continues once the provider's terms are accepted for the organization, either in the dashboard or by showing the user `firecrawl alexandria terms show ` and, only after they explicitly agree, running `firecrawl alexandria terms accept`. Approving does not accept them. +With `--require-approval` (needs `--mode chat`), a run can end on a `pendingApproval` instead of making a paid call. Ask the user, then answer it on the same thread with `firecrawl agent "" --thread --mode chat --approve ` (or `--decline `). A `terms` approval only continues once the provider's terms are accepted for the organization, either in the dashboard or by showing the user `firecrawl alexandria terms show ` and, only after they explicitly agree, running `firecrawl alexandria terms accept --terms-version --digest --confirm` with the version and digest it returned. Approving does not accept them. ## Job IDs diff --git a/src/__tests__/alexandria-beta.test.ts b/src/__tests__/alexandria-beta.test.ts index 00f24e6adc..6e42bb9d74 100644 --- a/src/__tests__/alexandria-beta.test.ts +++ b/src/__tests__/alexandria-beta.test.ts @@ -1152,6 +1152,18 @@ it('rejects Alexandria flags the API cannot honor before calling it', async () = expect(both.code).toBe(1); expect(both.stderr).toContain('use --approve or --decline, not both'); + const alwaysWithoutApprove = await cli([ + 'agent', + 'Keep going.', + '--thread', + THREAD_ID, + '--always', + ]); + expect(alwaysWithoutApprove.code).toBe(1); + expect(alwaysWithoutApprove.stderr).toContain( + '--call-ids and --always only apply with --approve' + ); + const declineWithCallIds = await cli([ 'agent', 'Never mind.', @@ -1159,8 +1171,7 @@ it('rejects Alexandria flags the API cannot honor before calling it', async () = THREAD_ID, '--decline', APPROVAL_ID, - '--call-ids', - 'call-1', + '--call-ids=', ]); expect(declineWithCallIds.code).toBe(1); expect(declineWithCallIds.stderr).toContain( @@ -1307,8 +1318,9 @@ it('shows a pending approval and how to answer it', async () => { const terms = await cli(['agent', RUN_ID]); expect(terms.stdout).toContain('Approving does not accept terms.'); expect(terms.stdout).toContain( - ' - Crunchbase (crunchbase): https://example.com/terms/crunchbase' + ' - Crunchbase: https://example.com/terms/crunchbase' ); + expect(terms.stdout).toContain('firecrawl alexandria terms show crunchbase'); expect(terms.stdout).toContain(`--approve ${APPROVAL_ID}`); }); diff --git a/src/commands/agent.ts b/src/commands/agent.ts index faa6d0ba7a..3b000313e4 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -523,10 +523,13 @@ function pendingApprovalLines( const lines = [`Pending approval ${approval.id}: ${approval.reason}`]; if (approval.kind === 'terms') { lines.push( - 'Approving does not accept terms. Accept them first with firecrawl alexandria terms accept , or in the dashboard:' + 'Approving does not accept terms. Accept them first in the dashboard, or review them with terms show and, once agreed, accept the version and digest it returns with firecrawl alexandria terms accept --terms-version --digest --confirm:' ); for (const gate of approval.terms) { - lines.push(` - ${gate.name} (${gate.provider}): ${gate.url}`); + lines.push( + ` - ${gate.name}: ${gate.url}`, + ` firecrawl alexandria terms show ${gate.provider}` + ); } } else { for (const call of approval.calls) { diff --git a/src/index.ts b/src/index.ts index 331e612cc0..b58c951486 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1766,7 +1766,10 @@ function createAgentCommand(): Command { ); process.exit(1); } - if ((options.callIds || options.always) && !options.approve) { + if ( + (options.callIds !== undefined || options.always) && + !options.approve + ) { console.error( 'Error: --call-ids and --always only apply with --approve.' );