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..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 `--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 --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 abb375c056..6e42bb9d74 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,32 @@ 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 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.', + '--thread', + THREAD_ID, + '--decline', + APPROVAL_ID, + '--call-ids=', + ]); + 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 +1185,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 +1316,11 @@ 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: 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 81f4aca5af..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 in the Firecrawl dashboard first:' + '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.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 1c8c0835c5..b58c951486 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,15 @@ function createAgentCommand(): Command { ); process.exit(1); } + if ( + (options.callIds !== undefined || 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 +1834,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,