@@ -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 */
171400function 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