Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,12 @@ Requests from the CLI are tagged `integration: cli`. A tool that wraps the CLI c
export FIRECRAWL_INTEGRATION=_my-tool
```

Supported API requests carry an origin of `cli@<version>`. When a coding agent runs the CLI, the origin also names the agent, for example `cli.codex@<version>`; it is detected from environment variables the agent sets in the commands it runs. Calls routed through the SDK include the agent only after the pinned SDK supports the origin option. To send plain `cli@<version>` instead:

```bash
export FIRECRAWL_CLI_DISABLE_AGENT_ORIGIN=1
```

---

## Experimental
Expand Down
37 changes: 21 additions & 16 deletions src/__tests__/alexandria-beta.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { afterAll, beforeAll, beforeEach, expect, it } from 'vitest';
import packageJson from '../../package.json';

const exec = promisify(execFile);
const requests: { url?: string; headers: Record<string, any>; body: any }[] =
Expand Down Expand Up @@ -1868,28 +1869,32 @@ it('forwards URL scrape discovery detail and rejects invalid combinations locall

it('submits Alexandria session feedback without a job ID', async () => {
response = { success: true, feedbackId: 'feedback-1', creditsRefunded: 0 };
const result = await cli([
'alexandria',
'feedback',
'--rating',
'partial',
'--url',
'https://example.com',
'--requested-functionality',
'Download attachments',
'--objective',
' Compare contract requirements across agencies ',
'--rationale',
'Only summaries available',
'--json',
]);
const result = await cli(
[
'alexandria',
'feedback',
'--rating',
'partial',
'--url',
'https://example.com',
'--requested-functionality',
'Download attachments',
'--objective',
' Compare contract requirements across agencies ',
'--rationale',
'Only summaries available',
'--json',
],
'fc-test',
{ FIRECRAWL_CLI_DISABLE_AGENT_ORIGIN: '1' }
);
expect(result.code).toBe(0);
expect(JSON.parse(result.stdout).feedbackId).toBe('feedback-1');
expect(requests[0].url).toBe('/v2/feedback');
expect(requests[0].body).toEqual({
endpoint: 'alexandria',
rating: 'partial',
origin: 'cli',
origin: `cli@${packageJson.version}`,
integration: 'cli',
requestedWebsite: {
url: 'https://example.com',
Expand Down
8 changes: 6 additions & 2 deletions src/__tests__/commands/developer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { initializeConfig } from '../../utils/config';
import { writeOutput } from '../../utils/output';
import { setupTest, teardownTest } from '../utils/mock-client';

vi.mock('../../utils/agent-origin', () => ({
getCliOrigin: () => 'cli.test@1.0.0',
}));

vi.mock('../../utils/output', () => ({ writeOutput: vi.fn() }));

vi.mock('../../utils/client', async () => {
Expand Down Expand Up @@ -67,7 +71,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&integration=cli&origin=cli.test%401.0.0'
);
});

Expand All @@ -80,7 +84,7 @@ 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&integration=cli&origin=cli.test%401.0.0'
);
});

Expand Down
12 changes: 8 additions & 4 deletions src/__tests__/commands/feedback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { getClient } from '../../utils/client';
import { initializeConfig } from '../../utils/config';
import { setupTest, teardownTest } from '../utils/mock-client';

vi.mock('../../utils/agent-origin', () => ({
getCliOrigin: () => 'cli.test@1.0.0',
}));

vi.mock('../../utils/client', async () => {
const actual = await vi.importActual('../../utils/client');
return {
Expand Down Expand Up @@ -85,7 +89,7 @@ describe('executeEndpointFeedback', () => {
expect(JSON.parse(request.body)).toEqual({
endpoint: 'alexandria',
rating: 'partial',
origin: 'cli',
origin: 'cli.test@1.0.0',
integration: 'cli',
requestedWebsite,
rationale: 'Only summaries available',
Expand Down Expand Up @@ -149,7 +153,7 @@ describe('executeEndpointFeedback', () => {
endpoint: 'scrape',
jobId: '0193f6c5-1234-7890-abcd-1234567890ab',
rating: 'partial',
origin: 'cli',
origin: 'cli.test@1.0.0',
integration: 'cli',
issues: ['missing_markdown'],
tags: ['docs'],
Expand Down Expand Up @@ -183,7 +187,7 @@ describe('executeEndpointFeedback', () => {
expect(endpointUrl).toBe('https://api.firecrawl.dev/v2/feedback');
expect(endpointRequest.headers['X-Integration']).toBe('rtk');
expect(JSON.parse(endpointRequest.body)).toMatchObject({
origin: 'cli',
origin: 'cli.test@1.0.0',
integration: 'cli',
});
const [searchUrl, searchRequest] = mockFetch.mock.calls[1];
Expand All @@ -193,7 +197,7 @@ describe('executeEndpointFeedback', () => {
expect(searchRequest.headers['X-Integration']).toBe('rtk');
expect(JSON.parse(searchRequest.body)).toEqual({
rating: 'good',
origin: 'cli',
origin: 'cli.test@1.0.0',
integration: 'cli',
});
});
Expand Down
16 changes: 13 additions & 3 deletions src/__tests__/commands/gov.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { initializeConfig } from '../../utils/config';
import { writeOutput } from '../../utils/output';
import { setupTest, teardownTest } from '../utils/mock-client';

vi.mock('../../utils/agent-origin', () => ({
getCliOrigin: () => 'cli.test@1.0.0',
}));

vi.mock('../../utils/output', () => ({ writeOutput: vi.fn() }));

vi.mock('../../utils/client', async () => {
Expand Down Expand Up @@ -56,8 +60,14 @@ describe('handleGovSearchCommand', () => {

describe('API call generation', () => {
it.each([
[{}, '/v2/search/gov?query=food+labeling&integration=cli'],
[{ k: 5 }, '/v2/search/gov?query=food+labeling&k=5&integration=cli'],
[
{},
'/v2/search/gov?query=food+labeling&integration=cli&origin=cli.test%401.0.0',
],
[
{ k: 5 },
'/v2/search/gov?query=food+labeling&k=5&integration=cli&origin=cli.test%401.0.0',
],
])('calls /v2/search/gov with %o', async (extra, expectedUrl) => {
mockHttpGet.mockResolvedValue(mockGovResponse([sampleResult]));

Expand Down Expand Up @@ -140,7 +150,7 @@ describe('handleGovSearchCommand', () => {

expect(mockHttpGet).not.toHaveBeenCalled();
expect(fetchMock).toHaveBeenCalledWith(
'https://api.firecrawl.dev/v2/search/gov?query=food+labeling&integration=cli',
'https://api.firecrawl.dev/v2/search/gov?query=food+labeling&integration=cli&origin=cli.test%401.0.0',
expect.objectContaining({ method: 'GET' })
);
expect(vi.mocked(writeOutput).mock.calls[0][0]).toContain(
Expand Down
52 changes: 52 additions & 0 deletions src/__tests__/commands/interact.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { handleInteractExecute } from '../../commands/interact';
import { initializeConfig } from '../../utils/config';
import { setupTest, teardownTest } from '../utils/mock-client';

vi.mock('../../utils/agent-origin', () => ({
getCliOrigin: () => 'cli.test@1.0.0',
}));

vi.mock('../../utils/output', () => ({ writeOutput: vi.fn() }));

describe('handleInteractExecute', () => {
let mockFetch: ReturnType<typeof vi.fn>;

beforeEach(() => {
setupTest();
initializeConfig({
apiKey: 'test-api-key',
apiUrl: 'https://api.firecrawl.dev',
});
mockFetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ success: true, stdout: 'ok' }),
});
vi.stubGlobal('fetch', mockFetch);
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
});

afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
teardownTest();
});

it('sends the CLI origin and integration in the request body', async () => {
await handleInteractExecute({
scrapeId: 'scrape-1',
code: 'console.log(1)',
});

expect(mockFetch).toHaveBeenCalledTimes(1);
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit];
expect(url).toBe('https://api.firecrawl.dev/v2/scrape/scrape-1/interact');
expect(JSON.parse(init.body as string)).toEqual({
origin: 'cli.test@1.0.0',
integration: 'cli',
code: 'console.log(1)',
language: 'node',
});
});
});
5 changes: 5 additions & 0 deletions src/__tests__/commands/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { executeParse } from '../../commands/parse';
import { initializeConfig } from '../../utils/config';
import { setupTest, teardownTest } from '../utils/mock-client';

vi.mock('../../utils/agent-origin', () => ({
getCliOrigin: () => 'cli.test@1.0.0',
}));

vi.mock('../../utils/credentials', () => ({
loadCredentials: vi.fn(() => null),
}));
Expand Down Expand Up @@ -68,6 +72,7 @@ describe('executeParse', () => {
expect(options).toEqual({
formats: ['markdown'],
integration: 'cli',
origin: 'cli.test@1.0.0',
});
expect(init.body.get('file')).toBeInstanceOf(Blob);
});
Expand Down
116 changes: 116 additions & 0 deletions src/__tests__/utils/agent-origin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { describe, it, expect, afterEach, vi } from 'vitest';
import packageJson from '../../../package.json';
import { buildCliOrigin, detectAgent } from '../../utils/agent-origin';

const version = packageJson.version;

describe('detectAgent', () => {
it.each([
[{ CLAUDECODE: '1' }, 'claude-code'],
[{ CLAUDE_CODE_ENTRYPOINT: 'cli' }, 'claude-code'],
[{ CODEX_SANDBOX: 'seatbelt' }, 'codex'],
[{ CODEX_SANDBOX_NETWORK_DISABLED: '1' }, 'codex'],
[{ CODEX_THREAD_ID: 'abc' }, 'codex'],
[{ CODEX_MANAGED_BY_NPM: '1' }, 'codex'],
Comment thread
capy-ai[bot] marked this conversation as resolved.
[{ CODEX_MANAGED_BY_BUN: '1' }, 'codex'],
[{ CODEX_MANAGED_BY_PNPM: '1' }, 'codex'],
[{ CODEX_MANAGED_BY_VITE_PLUS: '1' }, 'codex'],
[{ CURSOR_AGENT: '1' }, 'cursor'],
[{ GEMINI_CLI: '1' }, 'gemini-cli'],
[{ OPENCODE: '1' }, 'opencode'],
[{ GOOSE_TERMINAL: '1' }, 'goose'],
[{ CLINE_ACTIVE: 'true' }, 'cline'],
])('maps %j to %s', (env, slug) => {
expect(detectAgent(env)).toBe(slug);
});

it('returns undefined without agent variables', () => {
expect(detectAgent({ PATH: '/usr/bin', TERM_PROGRAM: 'vscode' })).toBe(
undefined
);
});

it('ignores empty and falsy values', () => {
expect(detectAgent({ CLAUDECODE: '', GEMINI_CLI: '0' })).toBe(undefined);
expect(detectAgent({ OPENCODE: 'false' })).toBe(undefined);
});

it('prefers the innermost agent when several are detected', () => {
expect(detectAgent({ CLAUDECODE: '1', OPENCODE: '1' })).toBe('opencode');
});

it.each(['1', 'true', 'YES', 'on'])(
'returns undefined when the opt-out is %s',
(value) => {
expect(
detectAgent({
CLAUDECODE: '1',
FIRECRAWL_CLI_DISABLE_AGENT_ORIGIN: value,
})
).toBe(undefined);
}
);

it.each(['0', 'false', 'no', 'off', ''])(
'keeps the agent when the opt-out is %j',
(value) => {
expect(
detectAgent({
CLAUDECODE: '1',
FIRECRAWL_CLI_DISABLE_AGENT_ORIGIN: value,
})
).toBe('claude-code');
}
);
});

describe('buildCliOrigin', () => {
it('stamps the agent before the version', () => {
expect(buildCliOrigin({ CURSOR_AGENT: '1' })).toBe(`cli.cursor@${version}`);
});

it('falls back to plain cli@<version>', () => {
expect(buildCliOrigin({})).toBe(`cli@${version}`);
expect(
buildCliOrigin({
GEMINI_CLI: '1',
FIRECRAWL_CLI_DISABLE_AGENT_ORIGIN: '1',
})
).toBe(`cli@${version}`);
});
});

describe('getCliOrigin', () => {
afterEach(() => {
vi.unstubAllEnvs();
vi.resetModules();
});

it('computes the origin once per process', async () => {
for (const name of [
'CLAUDECODE',
'CLAUDE_CODE_ENTRYPOINT',
'CODEX_SANDBOX',
'CODEX_SANDBOX_NETWORK_DISABLED',
'CODEX_THREAD_ID',
'CODEX_MANAGED_BY_NPM',
'CODEX_MANAGED_BY_BUN',
'CODEX_MANAGED_BY_PNPM',
'CODEX_MANAGED_BY_VITE_PLUS',
'CURSOR_AGENT',
'OPENCODE',
'GOOSE_TERMINAL',
'CLINE_ACTIVE',
'FIRECRAWL_CLI_DISABLE_AGENT_ORIGIN',
])
vi.stubEnv(name, '');
vi.stubEnv('GEMINI_CLI', '1');
Comment thread
capy-ai[bot] marked this conversation as resolved.
vi.resetModules();
const { getCliOrigin } = await import('../../utils/agent-origin');
expect(getCliOrigin()).toBe(`cli.gemini-cli@${version}`);

vi.stubEnv('GEMINI_CLI', '');
vi.stubEnv('OPENCODE', '1');
expect(getCliOrigin()).toBe(`cli.gemini-cli@${version}`);
});
});
Loading
Loading