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
2 changes: 2 additions & 0 deletions src/commands/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as fs from 'fs';
import { Command } from 'commander';
import { getConfig, validateConfig } from '../utils/config';
import { writeOutput } from '../utils/output';
import { getSessionHeaders } from '../utils/session-tracking';

const DEFAULT_API_URL = 'https://api.firecrawl.dev';

Expand Down Expand Up @@ -63,6 +64,7 @@ async function monitorRequest(
const headers: Record<string, string> = {
Authorization: `Bearer ${apiKey}`,
'X-Origin': 'cli',
...getSessionHeaders(),
};
if (init.body !== undefined) headers['Content-Type'] = 'application/json';

Expand Down
6 changes: 5 additions & 1 deletion src/commands/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { ScrapeFormat } from '../types/scrape';
import { getClient } from '../utils/client';
import { getConfig, validateConfig } from '../utils/config';
import { handleScrapeOutput } from '../utils/output';
import { getSessionHeaders } from '../utils/session-tracking';

const DEFAULT_API_URL = 'https://api.firecrawl.dev';

Expand Down Expand Up @@ -181,7 +182,10 @@ export async function executeParse(
try {
const response = await fetch(`${apiUrl}/v2/parse`, {
method: 'POST',
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,
headers: {
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
...getSessionHeaders(),
},
body: form,
});

Expand Down
34 changes: 32 additions & 2 deletions src/utils/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,39 @@ import {
updateConfig,
type GlobalConfig,
} from './config';
import { getSessionHeaders } from './session-tracking';

let clientInstance: Firecrawl | null = null;

function attachSessionHeaders(client: Firecrawl): Firecrawl {
const headers = getSessionHeaders();
if (Object.keys(headers).length === 0) return client;

const httpClient = (
client as unknown as {
http?: {
instance?: {
defaults?: {
headers?: Record<string, unknown> & {
common?: Record<string, unknown>;
};
};
};
};
}
).http;
const defaultHeaders = httpClient?.instance?.defaults?.headers;
if (!defaultHeaders) return client;

if (!defaultHeaders.common) {
defaultHeaders.common = {};
}
for (const [key, value] of Object.entries(headers)) {
(defaultHeaders.common as Record<string, unknown>)[key] = value;
}
return client;
}

/**
* Get or create the Firecrawl client instance
* Uses global configuration if available, otherwise creates with provided options
Expand Down Expand Up @@ -68,7 +98,7 @@ export function getClient(
backoffFactor: options.backoffFactor ?? config.backoffFactor,
};

return new Firecrawl(clientOptions);
return attachSessionHeaders(new Firecrawl(clientOptions));
}

// Return singleton instance or create one
Expand All @@ -84,7 +114,7 @@ export function getClient(
backoffFactor: config.backoffFactor,
};

clientInstance = new Firecrawl(clientOptions);
clientInstance = attachSessionHeaders(new Firecrawl(clientOptions));
}

return clientInstance;
Expand Down
67 changes: 67 additions & 0 deletions src/utils/session-tracking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as fs from 'fs';
import * as path from 'path';
import { randomUUID } from 'crypto';
import { getConfigDirectoryPath } from './credentials';

const SESSION_FILE = 'session.json';
const SESSION_TTL_MS = 30 * 60 * 1000;

type StoredSession = { id: string; last_active_at: number };

function sessionFilePath(): string {
return path.join(getConfigDirectoryPath(), SESSION_FILE);
}

function readSession(): StoredSession | null {
try {
const raw = fs.readFileSync(sessionFilePath(), 'utf8');
const parsed = JSON.parse(raw);
if (
parsed &&
typeof parsed.id === 'string' &&
typeof parsed.last_active_at === 'number'
) {
return parsed as StoredSession;
}
} catch {
// ignore
}
return null;
}

function writeSession(session: StoredSession): void {
try {
const dir = path.dirname(sessionFilePath());
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(sessionFilePath(), JSON.stringify(session));
} catch {
// best-effort
}
}

let cachedSessionId: string | null = null;

export function getSessionId(): string {
if (cachedSessionId) return cachedSessionId;

const now = Date.now();
const existing = readSession();
const id =
existing && now - existing.last_active_at < SESSION_TTL_MS
? existing.id
: randomUUID();

writeSession({ id, last_active_at: now });
cachedSessionId = id;
return id;
}

export type SessionHeaders = Record<string, string>;

export function getSessionHeaders(): SessionHeaders {
return {
'x-firecrawl-session-id': getSessionId(),
};
}
Loading