Skip to content

Commit e8cc94b

Browse files
committed
Nick:
1 parent 4e906c4 commit e8cc94b

File tree

2 files changed

+290
-9
lines changed

2 files changed

+290
-9
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,23 @@ firecrawl crawl https://docs.example.com --wait -o docs.json
441441

442442
---
443443

444+
## Telemetry
445+
446+
The CLI collects anonymous usage data during authentication to help improve the product:
447+
448+
- CLI version, OS, and Node.js version
449+
- Detect development tools (e.g., Cursor, VS Code, Claude Code)
450+
451+
**No command data, URLs, or file contents are collected via the CLI.**
452+
453+
To disable telemetry, set the environment variable:
454+
455+
```bash
456+
export FIRECRAWL_NO_TELEMETRY=1
457+
```
458+
459+
---
460+
444461
## Documentation
445462

446463
For more details, visit the [Firecrawl Documentation](https://docs.firecrawl.dev).

src/utils/auth.ts

Lines changed: 273 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -165,21 +165,261 @@ async function waitForAuth(
165165
});
166166
}
167167

168+
/**
169+
* Detect AI coding agents/IDEs that might be using the CLI
170+
* Returns all detected agent names since users may have multiple installed
171+
*/
172+
function detectCodingAgents(): string[] {
173+
try {
174+
const agents: string[] = [];
175+
const fs = require('fs');
176+
const path = require('path');
177+
const os = require('os');
178+
179+
// Environment variable detection
180+
// Based on documented env vars from official sources
181+
const envDetections: Array<{
182+
name: string;
183+
envVars: string[];
184+
}> = [
185+
// Aider: Well-documented env vars - https://aider.chat/docs/config/options.html
186+
{
187+
name: 'aider',
188+
envVars: ['AIDER_MODEL', 'AIDER_WEAK_MODEL', 'AIDER_EDITOR_MODEL'],
189+
},
190+
// Codex (OpenAI): Uses CODEX_HOME for config - https://developers.openai.com/codex/config-advanced/
191+
{
192+
name: 'codex',
193+
envVars: ['CODEX_HOME'],
194+
},
195+
// OpenCode: Documented env vars - https://opencode.ai/docs/config/
196+
{
197+
name: 'opencode',
198+
envVars: [
199+
'OPENCODE_CONFIG',
200+
'OPENCODE_CONFIG_DIR',
201+
'OPENCODE_CONFIG_CONTENT',
202+
],
203+
},
204+
// VS Code: Standard VS Code env vars set in integrated terminal
205+
{
206+
name: 'vscode',
207+
envVars: [
208+
'VSCODE_PID',
209+
'VSCODE_CWD',
210+
'VSCODE_IPC_HOOK',
211+
'VSCODE_GIT_IPC_HANDLE',
212+
],
213+
},
214+
// Zed: Terminal indicator
215+
{
216+
name: 'zed',
217+
envVars: ['ZED_TERM'],
218+
},
219+
];
220+
221+
// Check TERM_PROGRAM for terminal-based detection
222+
// IDEs often set this when spawning integrated terminals
223+
const termProgram = process.env.TERM_PROGRAM?.toLowerCase();
224+
if (termProgram) {
225+
if (termProgram.includes('cursor')) {
226+
agents.push('cursor');
227+
} else if (termProgram.includes('windsurf')) {
228+
agents.push('windsurf');
229+
} else if (termProgram.includes('vscode') || termProgram === 'vscode') {
230+
agents.push('vscode');
231+
} else if (termProgram.includes('zed')) {
232+
agents.push('zed');
233+
}
234+
}
235+
236+
// Check specific environment variables
237+
for (const detection of envDetections) {
238+
// Skip if already detected via TERM_PROGRAM
239+
if (agents.includes(detection.name)) continue;
240+
241+
const hasEnvVar = detection.envVars.some((envVar) => process.env[envVar]);
242+
if (hasEnvVar) {
243+
agents.push(detection.name);
244+
}
245+
}
246+
247+
// Config directory detection (check home directory)
248+
const homeDir = os.homedir();
249+
const cwd = process.cwd();
250+
251+
// Config directory detection
252+
// Based on official documentation for each tool
253+
const configDirDetections: Array<{
254+
name: string;
255+
dirs: string[];
256+
// If set, check for this file/subdirectory for a stronger signal
257+
indicator?: string;
258+
// Check home directory config path (some tools use ~/.config/toolname)
259+
homeConfigPath?: string;
260+
}> = [
261+
// Cursor: Uses .cursor/ directory - https://docs.cursor.com
262+
{
263+
name: 'cursor',
264+
dirs: ['.cursor'],
265+
},
266+
// Claude Code: Uses .claude/ with settings files - https://docs.anthropic.com/claude-code
267+
// Strong indicator: .claude/settings.json or .claude/settings.local.json
268+
{
269+
name: 'claude-code',
270+
dirs: ['.claude'],
271+
indicator: 'settings.json',
272+
},
273+
// VS Code: Uses .vscode/ for workspace settings
274+
{
275+
name: 'vscode',
276+
dirs: ['.vscode'],
277+
},
278+
// GitHub Copilot: VS Code extension, check for copilot config
279+
{
280+
name: 'github-copilot',
281+
dirs: ['.github'],
282+
indicator: 'copilot-instructions.md',
283+
},
284+
// OpenCode: Uses .opencode/ in project, ~/.config/opencode/ globally
285+
// https://opencode.ai/docs/config/
286+
{
287+
name: 'opencode',
288+
dirs: ['.opencode'],
289+
homeConfigPath: '.config/opencode',
290+
},
291+
// Codex (OpenAI): Uses ~/.codex/ for config
292+
// https://developers.openai.com/codex/config-advanced/
293+
{
294+
name: 'codex',
295+
dirs: ['.codex'],
296+
},
297+
// Continue: Uses ~/.continue/ for config
298+
// https://docs.continue.dev/setup/configuration
299+
{
300+
name: 'continue',
301+
dirs: ['.continue'],
302+
indicator: 'config.yaml',
303+
},
304+
// Aider: Uses .aider.conf.yml config files (not a directory)
305+
// https://aider.chat/docs/config/options.html
306+
{
307+
name: 'aider',
308+
dirs: ['.aider.conf.yml'],
309+
},
310+
// Windsurf: VS Code fork, may use .windsurf/
311+
{
312+
name: 'windsurf',
313+
dirs: ['.windsurf'],
314+
},
315+
// Cline: VS Code extension, stores in VS Code settings
316+
// May have .cline/ for project settings
317+
{
318+
name: 'cline',
319+
dirs: ['.cline'],
320+
},
321+
// Roo Code: VS Code extension, may have .roo/ or uses VS Code settings
322+
{
323+
name: 'roo-code',
324+
dirs: ['.roo'],
325+
},
326+
];
327+
328+
for (const detection of configDirDetections) {
329+
// Skip if already detected
330+
if (agents.includes(detection.name)) continue;
331+
332+
let found = false;
333+
334+
// Check homeConfigPath first (e.g., ~/.config/opencode/)
335+
if (detection.homeConfigPath) {
336+
try {
337+
const configPath = path.join(homeDir, detection.homeConfigPath);
338+
if (fs.existsSync(configPath)) {
339+
found = true;
340+
}
341+
} catch {
342+
// Ignore permission errors
343+
}
344+
}
345+
346+
// Check standard directories
347+
if (!found) {
348+
for (const dir of detection.dirs) {
349+
// Check in current working directory (project-level config)
350+
const cwdPath = path.join(cwd, dir);
351+
// Check in home directory (global config)
352+
const homePath = path.join(homeDir, dir);
353+
354+
try {
355+
// If indicator is specified, check for that file/subdir for a stronger signal
356+
if (detection.indicator) {
357+
const cwdIndicator = path.join(cwdPath, detection.indicator);
358+
const homeIndicator = path.join(homePath, detection.indicator);
359+
if (fs.existsSync(cwdIndicator) || fs.existsSync(homeIndicator)) {
360+
found = true;
361+
break;
362+
}
363+
}
364+
365+
// Check if the directory/file itself exists
366+
if (fs.existsSync(cwdPath) || fs.existsSync(homePath)) {
367+
found = true;
368+
break;
369+
}
370+
} catch {
371+
// Ignore permission errors
372+
}
373+
}
374+
}
375+
376+
if (found) {
377+
agents.push(detection.name);
378+
}
379+
}
380+
381+
return agents;
382+
} catch (error) {
383+
console.error('Error detecting coding agents:', error);
384+
return [];
385+
}
386+
}
387+
388+
/**
389+
* Check if telemetry is disabled via environment variable
390+
*/
391+
function isTelemetryDisabled(): boolean {
392+
const noTelemetry = process.env.FIRECRAWL_NO_TELEMETRY;
393+
return noTelemetry === '1' || noTelemetry === 'true';
394+
}
395+
168396
/**
169397
* Get CLI metadata for telemetry
398+
* Returns null if telemetry is disabled
170399
*/
171400
function getCliMetadata(): {
172401
cli_version: string;
173402
os_platform: string;
174403
node_version: string;
175-
} {
404+
detected_agents: string;
405+
} | null {
406+
// Check if telemetry is disabled
407+
if (isTelemetryDisabled()) {
408+
return null;
409+
}
410+
176411
// Dynamic import to avoid circular dependencies
177412
// eslint-disable-next-line @typescript-eslint/no-var-requires
178413
const packageJson = require('../../package.json');
414+
415+
// Detect coding agents
416+
const agents = detectCodingAgents();
417+
179418
return {
180419
cli_version: packageJson.version || 'unknown',
181420
os_platform: process.platform,
182421
node_version: process.version,
422+
detected_agents: agents.join(',') || 'unknown',
183423
};
184424
}
185425

@@ -199,16 +439,22 @@ async function browserLogin(
199439
const codeChallenge = generateCodeChallenge(codeVerifier);
200440

201441
// Get CLI metadata for telemetry (non-sensitive)
442+
// Returns null if telemetry is disabled via FIRECRAWL_NO_TELEMETRY
202443
const metadata = getCliMetadata();
203-
const telemetryParams = new URLSearchParams({
204-
cli_version: metadata.cli_version,
205-
os_platform: metadata.os_platform,
206-
node_version: metadata.node_version,
207-
}).toString();
208444

209-
// code_challenge and telemetry in query (safe - not sensitive)
210-
// session_id in fragment (not sent to server, read by JS only)
211-
const loginUrl = `${webUrl}/cli-auth?code_challenge=${codeChallenge}&${telemetryParams}#session_id=${sessionId}`;
445+
let loginUrl: string;
446+
if (metadata) {
447+
const telemetryParams = new URLSearchParams({
448+
cli_version: metadata.cli_version,
449+
os_platform: metadata.os_platform,
450+
node_version: metadata.node_version,
451+
detected_agents: metadata.detected_agents,
452+
}).toString();
453+
loginUrl = `${webUrl}/cli-auth?code_challenge=${codeChallenge}&${telemetryParams}#session_id=${sessionId}`;
454+
} else {
455+
// Telemetry disabled - don't send metadata
456+
loginUrl = `${webUrl}/cli-auth?code_challenge=${codeChallenge}#session_id=${sessionId}`;
457+
}
212458

213459
console.log('\nOpening browser for authentication...');
214460
console.log(`If the browser doesn't open, visit: ${loginUrl}\n`);
@@ -310,6 +556,7 @@ async function interactiveLogin(
310556
console.log(' \x1b[1m2.\x1b[0m Enter API key manually');
311557
console.log('');
312558
printEnvHint();
559+
printTelemetryNotice();
313560

314561
const choice = await promptInput('Enter choice [1/2]: ');
315562

@@ -331,6 +578,23 @@ function printEnvHint(): void {
331578
);
332579
}
333580

581+
/**
582+
* Print telemetry notice
583+
*/
584+
function printTelemetryNotice(): void {
585+
const dim = '\x1b[2m';
586+
const reset = '\x1b[0m';
587+
588+
if (isTelemetryDisabled()) {
589+
console.log(`${dim}Telemetry disabled${reset}\n`);
590+
} else {
591+
console.log(
592+
`${dim}Anonymous telemetry (OS, CLI version, dev tools) collected to improve the CLI.${reset}`
593+
);
594+
console.log(`${dim}Disable with FIRECRAWL_NO_TELEMETRY=1${reset}\n`);
595+
}
596+
}
597+
334598
/**
335599
* Export banner for use in other places
336600
*/

0 commit comments

Comments
 (0)