Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 42 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,9 +467,20 @@ Paper ids accept `pmid:`, `pmcid:`, `doi:`, and `arxiv:` forms, plus canonical `

### `feedback` - Send endpoint job feedback

Send concise feedback for a completed v2 `search`, `scrape`, `parse`, or `map`
job. For search-result quality, `search-feedback` is still the most guided
command; `feedback` is the generic endpoint/job surface.
Send optional evidence through `/v2/feedback`. Keyless `search`, `scrape`, and
`parse` jobs require `--rating`, `--task`, `--assessment`, and 1-20 observations
provided through `--observations` or `--observations-file`. Keyless Parse also requires `--doc-class born_digital|scanned|mixed|unknown` once per submission. Use the returned job
reference and evidence already available; no user interview or additional
investigation is required. Run `firecrawl feedback --help` for category fields.

Keyless feedback accepts one new submission per identity per UTC day across
Search, Scrape, Parse, and all clients. References expire after 24 hours.
Submitting feedback does not consume or restore operation allowance. Invitations
and references appear in metadata or stderr, preserving ordinary stdout.

Authenticated callers retain the existing fields. `search-feedback` remains an
authenticated Search command and cannot submit feedback for keyless jobs. The
following example uses the authenticated endpoint feedback contract:

```bash
firecrawl feedback scrape 0193f6c5-1234-7890-abcd-1234567890ab \
Expand All @@ -484,25 +495,37 @@ firecrawl feedback scrape 0193f6c5-1234-7890-abcd-1234567890ab \
Keep notes and metadata small. Do not send raw scrape or parse outputs as
feedback.

Set `FIRECRAWL_NO_ENDPOINT_FEEDBACK=1` to make `firecrawl feedback` skip
endpoint feedback calls silently.
Search: useful and irrelevant require a one-based position within the delivered group. source names the response group the position refers to: web, images, or news. It is required only when the job requested multiple sources; otherwise it defaults to web. The position must exist in that requested group. irrelevant requires reason: aggregator_over_official, off_topic, stale, wrong_content_type, snippet_misleading, or blocked_or_paywalled. vertical is required on missing and optional on useful/irrelevant: web_general, social, business, research, developer, news, government, finance, or other. missing may include topic (up to 200 characters) and knownSources (up to 20 HTTP(S) URLs). Do not submit engine attribution; it comes from the stored category tag at that position.

Scrape: kind correct, wrong_success, incomplete, or incorrect. wrong_success requires reason: blocked_shell, login_required, paywall, empty, wrong_page, stale, or wrong_locale. incomplete requires reason: partial_content, dynamic_content, pagination, main_content_stripped, or format_lost. incorrect requires reason: wrong, hallucinated, or missing_fields. correct has no reason. Optional location is up to 200 characters. No retryOutcome. Hard-failed Scrape jobs receive no feedback invitation.

Parse: `--doc-class` is required once per submission: born_digital, scanned, mixed, or unknown. Observation kind: correct, text_ocr, table, formula, chart_figure, reading_order, headers_footers, headings_formatting, completeness, images_dropped, or incorrect. text_ocr requires reason: misread_chars, garbled, or missing_text. table requires reason: structure, cells_glued, or digits. completeness requires reason: pages_missing, truncated_at_max_pages, or sections_dropped. incorrect requires reason: wrong, hallucinated, or missing_fields. Other kinds have no reason subtype. Optional page is a one-based positive integer.

Scrape and Parse: format must be a format type the job requested. It is required for output and source_comparison observations when multiple formats were requested; optional for expectation observations and single-format jobs. All observations retain detail and basis; source_comparison requires comparison: {reference, detail}.

Set `FIRECRAWL_NO_ENDPOINT_FEEDBACK=1` or `FIRECRAWL_DISABLE_ENDPOINT_FEEDBACK=1` to skip authenticated endpoint feedback calls. These flags do not suppress keyless invitations or submissions. The API controls keyless invitation frequency and eligibility. Submitting feedback remains optional and is never required for continued keyless access.

#### Feedback Options

| Option | Description |
| -------------------------------- | -------------------------------------------- |
| `--rating <rating>` | Required: `good`, `partial`, or `bad` |
| `--issues <codesOrJson>` | Comma-separated issue codes or JSON array |
| `--tags <codesOrJson>` | Comma-separated tags or JSON array |
| `--note <text>` | Short human-readable feedback |
| `--valuable-sources <json>` | JSON array of `{url, reason}` entries |
| `--missing-content <json>` | JSON array of `{topic, description}` entries |
| `--query-suggestions <text>` | Search/query improvement notes |
| `--url <url>` | Relevant URL for scrape or parse feedback |
| `--page-numbers <numbersOrJson>` | Comma-separated page numbers or JSON array |
| `--metadata <json>` | Small JSON object with extra context |
| `--metadata-file <path>` | Path to small metadata JSON object |
| `--silent` | Suppress output for background agent calls |
| Option | Description |
| -------------------------------- | ---------------------------------------------------- |
| `--rating <rating>` | Required: `good`, `partial`, or `bad` |
| `--task <text>` | Task intent, required for keyless feedback |
| `--doc-class <class>` | Document class, required for keyless Parse |
| `--assessment <text>` | Assessment, required for keyless feedback |
| `--observations <json>` | JSON array of category-specific keyless observations |
| `--observations-file <path>` | File containing the observations JSON array |
| `--issues <codesOrJson>` | Comma-separated issue codes or JSON array |
| `--tags <codesOrJson>` | Comma-separated tags or JSON array |
| `--note <text>` | Short human-readable feedback |
| `--valuable-sources <json>` | JSON array of `{url, reason}` entries |
| `--missing-content <json>` | JSON array of `{topic, description}` entries |
| `--query-suggestions <text>` | Search/query improvement notes |
| `--url <url>` | Relevant URL for scrape or parse feedback |
| `--page-numbers <numbersOrJson>` | Comma-separated page numbers or JSON array |
| `--metadata <json>` | Small JSON object with extra context |
| `--metadata-file <path>` | Path to small metadata JSON object |
| `--silent` | Suppress output for background agent calls |

---

Expand Down
24 changes: 21 additions & 3 deletions skills/firecrawl-search/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Run `firecrawl search --help` for the full option list.

`--categories developer` weighs the developer index beside ordinary web results in this same call (no passage control, no index filters). `--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:** results are saved under `.firecrawl/`, verified non-empty, processed for the request, and one feedback event is sent within the time window (unless opted out).
**Done when:** the search response has been checked and processed for the request. Feedback is optional and is not a completion requirement.

## Tips

Expand All @@ -38,9 +38,27 @@ Run `firecrawl search --help` for the full option list.
- Use `jq` to extract URLs or titles: `jq -r '.data.web[].url' .firecrawl/search.json`
- Naming convention: `.firecrawl/search-{query}.json` or `.firecrawl/search-{query}-scraped.json`

## Send feedback after every search (refunds 1 credit)
## Optional feedback

Search costs 2 credits. After you've actually used the results (or decided they were useless), send structured feedback with `firecrawl search-feedback <id>`. The first feedback per search refunds 1 credit and helps us improve search quality. **Do this once per search**, in the background, after you finish processing the results — it does not block your main task.
Choose the feedback contract that matches the search job's authentication. A returned `metadata.feedback` invitation identifies the keyless submission endpoint and job ID. Do not send the keyless fields to the authenticated Search feedback route, or add credentials to submit feedback for a keyless job.

### Keyless Search

Use `firecrawl feedback search <jobId>` with `--rating`, `--task`, `--assessment`, and `--observations-file`. The task describes what the search needed to answer; the assessment describes how well it answered that task. Supply 1-20 observations:

- Useful or irrelevant results: `kind`, `source` (`web`, `images`, or `news`), and one-based `position` within that delivered group.
- Missing information: `kind: "missing"`, `topic`, and optional `knownSources` URLs, only when already known.
- Every observation requires `detail` and `basis`: `output`, `source_comparison`, or `expectation`. A source comparison also requires `comparison: {reference, detail}`. Use expectations for gaps that have not been verified against a source.

Task, assessment, and observation detail each require 10-2000 characters. Use only evidence already available. Do not ask the user to complete an interview, investigate further, guess missing content, or diagnose causes merely to submit feedback. An empty result set can support a missing-information observation if the response includes an eligible job reference.

One new submission is accepted per keyless identity per UTC day across Search, Scrape, Parse, API, MCP, and CLI. Job references expire after 24 hours. Feedback does not consume or restore operation allowance. Do not submit after every search or retry a daily-limit rejection in a loop. Client feedback flags do not suppress keyless invitations or submissions. Submitting feedback remains optional.

Run `firecrawl feedback --help` for the full evidence contract. Old instructions using `firecrawl search-feedback` require authentication and do not work for keyless jobs.

### Authenticated Search

Authenticated callers can continue using `firecrawl search-feedback <id>` with `--valuable-sources`, `--missing-content`, and `--query-suggestions`. Its existing validation, feedback window, and refund policy are unchanged. If submitting, do so after processing the results, at most once per search, using only evidence already available.

**Opt out:** if `FIRECRAWL_NO_SEARCH_FEEDBACK=1` (or `FIRECRAWL_DISABLE_SEARCH_FEEDBACK=1`) is set, the CLI silently skips the call and never sends anything. Respect that — do not try to work around it. (Team admins can also disable this server-side; the API will return `feedbackErrorCode: "TEAM_OPTED_OUT"` and the CLI will exit 0 silently.)

Expand Down
10 changes: 7 additions & 3 deletions skills/firecrawl/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,13 @@ Single format outputs raw content. Multiple formats (e.g., `--format markdown,li

## Feedback

After using search results, send `firecrawl search-feedback` (the first feedback per search refunds 1 credit). The full pattern, guard, and rules live in [firecrawl-search](../firecrawl-search/SKILL.md).
Feedback is optional and must not interrupt the requested task. For keyless Search, Scrape, or Parse jobs, use `firecrawl feedback <endpoint> <jobId>` with `--rating`, `--task`, `--assessment`, and `--observations-file`. Use the job reference and optional invitation returned in metadata or stderr. Run `firecrawl feedback --help` for category-specific observations.

For non-search endpoint jobs, use `firecrawl feedback <endpoint> <jobId>` to send concise job-level feedback through `/v2/feedback`. Supported endpoints are `search`, `scrape`, `parse`, and `map`.
Use only evidence already available, without interviewing the user or doing extra investigation. Keyless submissions are limited to one per identity per UTC day across Search, Scrape, Parse, and all clients; job references expire after 24 hours. Feedback does not consume or restore operation allowance. Do not send legacy issue/note fields as a substitute for keyless observations.

Authenticated Search callers can continue using `firecrawl search-feedback` with its existing fields and policy. The two authentication modes use different request contracts; do not add credentials to submit feedback for a keyless job. Details live in [firecrawl-search](../firecrawl-search/SKILL.md).

Authenticated callers can use `firecrawl feedback <endpoint> <jobId>` with the existing issue/note fields for `search`, `scrape`, `parse`, and `map`. The following example is for authenticated feedback:

```bash
firecrawl feedback scrape "$SCRAPE_ID" \
Expand All @@ -124,7 +128,7 @@ firecrawl feedback scrape "$SCRAPE_ID" \

Keep generic feedback small: issue codes, tags, short notes, URLs, page numbers, and small metadata objects — never raw scrape/parse outputs or full page contents.

**Opt out:** `export FIRECRAWL_NO_ENDPOINT_FEEDBACK=1` makes the CLI skip every endpoint feedback call silently. Respect that flag — do not try to work around it.
**Authenticated feedback preference:** `FIRECRAWL_NO_ENDPOINT_FEEDBACK=1` or `FIRECRAWL_DISABLE_ENDPOINT_FEEDBACK=1` skips authenticated endpoint feedback calls. Respect these flags for authenticated jobs. Keyless jobs retain server-issued invitations and optional submissions regardless of these flags.

## Parallelization

Expand Down
25 changes: 25 additions & 0 deletions src/__tests__/cli-argv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ describe('CLI argv parsing', () => {
const cliPath = resolve(process.cwd(), 'dist/index.js');
const testWithBuiltCli = existsSync(cliPath) ? it : it.skip;

testWithBuiltCli(
'describes substantive keyless evidence in feedback help',
() => {
const result = spawnSync(
process.execPath,
[cliPath, 'feedback', '--help'],
{
cwd: process.cwd(),
encoding: 'utf8',
}
);
expect(result.status).toBe(0);
for (const field of [
'--task',
'--assessment',
'--observations-file',
'one-based position',
'source_comparison',
'UTC day',
]) {
expect(result.stdout).toContain(field);
}
}
);

testWithBuiltCli('lists the developer command in root help output', () => {
const result = spawnSync(process.execPath, [cliPath, '--help'], {
cwd: process.cwd(),
Expand Down
91 changes: 77 additions & 14 deletions src/__tests__/commands/feedback.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
executeEndpointFeedback,
parseEndpointFeedbackCliOptions,
handleEndpointFeedbackCommand,
parseEndpointFeedbackEndpoint,
parseFeedbackListArg,
parsePageNumbersArg,
} from '../../commands/feedback';
import { getClient } from '../../utils/client';
import { initializeConfig } from '../../utils/config';
import { setupTest, teardownTest } from '../utils/mock-client';

vi.mock('../../utils/client', async () => {
const actual = await vi.importActual('../../utils/client');
return {
...actual,
getClient: vi.fn(),
};
});
vi.mock('../../utils/credentials', () => ({
loadCredentials: vi.fn(() => null),
}));

describe('executeEndpointFeedback', () => {
let mockFetch: ReturnType<typeof vi.fn>;
Expand All @@ -35,10 +31,63 @@ describe('executeEndpointFeedback', () => {
afterEach(() => {
teardownTest();
vi.clearAllMocks();
vi.unstubAllEnvs();
delete process.env.FIRECRAWL_NO_ENDPOINT_FEEDBACK;
delete process.env.FIRECRAWL_DISABLE_ENDPOINT_FEEDBACK;
});

it.each([undefined, 'https://api.firecrawl.dev'])(
'submits keyless evidence despite authenticated opt-out with API URL %s',
async (apiUrl) => {
vi.stubEnv('FIRECRAWL_API_KEY', '');
vi.stubEnv('FIRECRAWL_NO_ENDPOINT_FEEDBACK', '1');
vi.stubEnv('FIRECRAWL_DISABLE_ENDPOINT_FEEDBACK', '1');
initializeConfig({
apiKey: undefined,
apiUrl: 'https://api.firecrawl.dev',
});
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
success: true,
feedbackId: 'feedback-1',
creditsRefunded: 0,
}),
});
const observations = [
{
kind: 'incorrect',
reason: 'missing_fields',
format: 'json',
basis: 'output',
detail: 'The table contains the expected column headings.',
page: 2,
},
];
const result = await executeEndpointFeedback({
apiUrl,
endpoint: 'parse',
docClass: 'born_digital',
jobId: '00000000-0000-4000-8000-000000000001',
rating: 'good',
task: 'Read the table headings',
assessment: 'The output preserved all table headings.',
observations,
});
expect(result.success).toBe(true);
const [, init] = mockFetch.mock.calls[0];
expect(init.headers.Authorization).toBeUndefined();
expect(JSON.parse(init.body)).toMatchObject({
endpoint: 'parse',
docClass: 'born_digital',
observations,
origin: 'cli',
integration: 'cli',
});
}
);

it('posts generic endpoint feedback to /v2/feedback', async () => {
mockFetch.mockResolvedValue({
ok: true,
Expand All @@ -64,10 +113,6 @@ describe('executeEndpointFeedback', () => {
apiUrl: 'http://localhost:3002',
});

expect(getClient).toHaveBeenCalledWith({
apiKey: undefined,
apiUrl: 'http://localhost:3002',
});
expect(result).toEqual({
success: true,
feedbackId: '0193f6c5-1234-7890-abcd-1234567890ab',
Expand Down Expand Up @@ -149,7 +194,6 @@ describe('executeEndpointFeedback', () => {
creditsRefunded: 0,
});

expect(getClient).not.toHaveBeenCalled();
expect(mockFetch).not.toHaveBeenCalled();
});

Expand Down Expand Up @@ -178,7 +222,6 @@ describe('executeEndpointFeedback', () => {

expect(stderrSpy).not.toHaveBeenCalled();
expect(stdoutSpy).not.toHaveBeenCalled();
expect(getClient).not.toHaveBeenCalled();
expect(mockFetch).not.toHaveBeenCalled();
} finally {
exitSpy.mockRestore();
Expand Down Expand Up @@ -208,3 +251,23 @@ describe('feedback parsing', () => {
expect(parsePageNumbersArg('[4,5]')).toEqual([4, 5]);
});
});

describe('keyless document class option', () => {
it.each(['born_digital', 'scanned', 'mixed', 'unknown'] as const)(
'preserves %s for submission',
(docClass) => {
expect(
parseEndpointFeedbackCliOptions({ rating: 'partial', docClass })
.docClass
).toBe(docClass);
}
);
it('rejects an unsupported class without changing authenticated defaults', () => {
expect(() =>
parseEndpointFeedbackCliOptions({ rating: 'partial', docClass: 'pdf' })
).toThrow('--doc-class');
expect(
parseEndpointFeedbackCliOptions({ rating: 'good' }).docClass
).toBeUndefined();
});
});
Loading
Loading