Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '18'
node-version: '22'

- name: Setup pnpm
uses: pnpm/action-setup@v4
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "firecrawl-cli",
"version": "1.23.4-alexandria-beta.1",
"version": "1.23.4-alexandria-beta.2",
"publishConfig": {
"tag": "alexandria"
},
Expand Down Expand Up @@ -67,7 +67,7 @@
},
"homepage": "https://docs.firecrawl.dev/cli",
"engines": {
"node": ">=18.0.0"
"node": ">=22.0.0"

@cubic-dev-ai cubic-dev-ai Bot Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The engines floor is now Node 22 but @types/node stays at ^20.0.0, so TypeScript type-checks against Node 20 typings even though the package requires Node 22. Bump the devDependency to ^22.0.0 so new code targeting Node 22+ APIs type-checks (e.g. fetch/undici, crypto.hash) instead of failing under @types/node 20 and forcing an unplanned bump later.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At package.json, line 70:

<comment>The engines floor is now Node 22 but @types/node stays at ^20.0.0, so TypeScript type-checks against Node 20 typings even though the package requires Node 22. Bump the devDependency to ^22.0.0 so new code targeting Node 22+ APIs type-checks (e.g. fetch/undici, crypto.hash) instead of failing under @types/node 20 and forcing an unplanned bump later.</comment>

<file context>
@@ -67,7 +67,7 @@
   "homepage": "https://docs.firecrawl.dev/cli",
   "engines": {
-    "node": ">=18.0.0"
+    "node": ">=22.0.0"
   },
   "files": [
</file context>
Fix with cubic

},
"files": [
"dist",
Expand All @@ -86,7 +86,7 @@
"dependencies": {
"@inquirer/prompts": "^8.2.1",
"commander": "^14.0.2",
"firecrawl": "4.24.0",
"firecrawl": "4.40.0",
"yaml": "^2.9.0",
"zod-to-json-schema": "3.24.6"
}
Expand Down
50 changes: 37 additions & 13 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 37 additions & 1 deletion src/__tests__/alexandria-beta.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,12 @@ it('relays terms refusals and keeps the request ID on failure', async () => {
success: false,
error: 'Accept provider terms',
code: 'THIRD_PARTY_DATA_TERMS_REQUIRED',
requiresAction: { url: 'https://firecrawl.dev/terms/provider' },
requiresAction: {
type: 'accept_terms',
terms: 'provider',
version: '1.0',
url: 'https://firecrawl.dev/terms/provider',
},
};
const result = await cli([
'scrape',
Expand Down Expand Up @@ -196,6 +201,37 @@ it('executes Find Tools through the same API and refuses keyless access', async
});
});

it('preserves charged SDK failures', async () => {
status = 402;
response = {
success: false,
error: 'Provider call failed after billing',
code: 'PROVIDER_ERROR',
chargeId: 'charge-1',
};
const result = await cli(['scrape', '--alexandria', 'provider/lookup']);
expect(result.code).toBe(1);
expect(JSON.parse(result.stdout)).toMatchObject(response);
expect(requests).toHaveLength(1);

@cubic-dev-ai cubic-dev-ai Bot Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This test does not verify that both provider calls were sent. Assert the request body contains provider/lookup and other/lookup; otherwise it can pass when the CLI submits only one call and the mock still returns the preloaded partial response.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/__tests__/alexandria-beta.test.ts, line 215:

<comment>This test does not verify that both provider calls were sent. Assert the request body contains `provider/lookup` and `other/lookup`; otherwise it can pass when the CLI submits only one call and the mock still returns the preloaded partial response.</comment>

<file context>
@@ -196,6 +201,37 @@ it('executes Find Tools through the same API and refuses keyless access', async
+  const result = await cli(['scrape', '--alexandria', 'provider/lookup']);
+  expect(result.code).toBe(1);
+  expect(JSON.parse(result.stdout)).toMatchObject(response);
+  expect(requests).toHaveLength(1);
+});
+
</file context>
Suggested change
expect(requests).toHaveLength(1);
expect(requests).toHaveLength(1);
expect(requests[0].body.alexandria).toEqual([
expect.objectContaining({ provider: 'provider', capability: 'lookup' }),
expect.objectContaining({ provider: 'other', capability: 'lookup' }),
]);
Fix with cubic

});

it('retains successful results and billing when one provider fails', async () => {
response.data.alexandria.push({
provider: 'other',
capability: 'lookup',
error: { code: 'PROVIDER_ERROR', message: 'Unavailable', status: 503 },
});
const result = await cli([
'scrape',
'--alexandria',
'provider/lookup',
'other/lookup',
]);
expect(result.code).toBe(1);
expect(JSON.parse(result.stdout)).toMatchObject(response);
expect(requests).toHaveLength(1);
});

it('keeps URL scrape tool contracts in the output', async () => {
response = {
success: true,
Expand Down
43 changes: 26 additions & 17 deletions src/commands/alexandria.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { randomUUID } from 'node:crypto';
import { Command, Option } from 'commander';
import { SdkError, type AlexandriaCall } from 'firecrawl';
import { getClient } from '../utils/client';
import { getApiKey } from '../utils/config';
import { writeOutput } from '../utils/output';

type Call = {
provider: string;
capability: string;
options: Record<string, unknown>;
};
type Call = AlexandriaCall & { options: Record<string, unknown> };
type Options = {
apiKey?: string;
apiUrl?: string;
Expand Down Expand Up @@ -55,7 +52,17 @@ export function buildCalls(addresses: string[], values: string[] = []): Call[] {
}

export function apiFailure(error: unknown): Record<string, unknown> {
const body = (error as any)?.response?.data;
const body =
(error as any)?.response?.data ??
(error instanceof SdkError
? {
error: error.message,
code: error.code,
chargeId: error.chargeId,
requiresAction:
(error.details as any)?.requiresAction ?? error.requiresAction,
}
: undefined);
return {
success: false,
error:
Expand Down Expand Up @@ -83,18 +90,20 @@ export async function handleAlexandria(
let envelope: Record<string, any>;
try {
const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl });
const response = await (app as any).http.post(
'/v2/scrape',
{
alexandria: calls,
integration: 'cli',
timeout: options.timeout,
const result = await app.scrape({
alexandria: calls,
integration: 'cli',
timeout: options.timeout,
requestId,
});
envelope = {
success: true,
...(result.scrapeId && { scrape_id: result.scrapeId }),
data: {
alexandria: result.alexandria,
creditsCost: result.creditsCost,
},
{ headers: { 'x-request-id': requestId } }
);
envelope = response.data;
if (!envelope || typeof envelope.success !== 'boolean')
throw new Error('Invalid Alexandria response.');
};
} catch (error) {
envelope = apiFailure(error);
}
Expand Down
Loading