From 846ff76348f6d2c251b2322f867a81d060726d7d Mon Sep 17 00:00:00 2001 From: Yash Dewasthale Date: Thu, 2 Jul 2026 23:50:35 +0530 Subject: [PATCH] feat: fix Firecrawl tools returning 'not configured' by loading .env before env var check Bun's auto .env loading only runs from CWD, so 'process.env.FIRECRAWL_API_KEY' was undefined when the CLI ran from outside the server directory. Every Firecrawl tool (search, scrape, map) and the chat tool selector would see 'not configured' even though the key existed in server/.env. - Refactored src/lib/load-env.ts into an exported callable 'loadEnvOnce()' that walks up to 7 directory candidates to find '.env' - Added 'loadEnvOnce()' call in firecrawl-search, firecrawl-scrape, firecrawl-map, web-search, and chat.ts before reading their respective env vars - Removed the duplicated inline 'loadEnvOnce()' from web-search.ts (~50 lines) --- apps/supercode-cli/server/package.json | 2 +- .../server/src/cli/ai/chat/chat.ts | 4 ++ apps/supercode-cli/server/src/lib/load-env.ts | 58 ++++++++++++------- .../src/tools/definitions/firecrawl-map.ts | 2 + .../src/tools/definitions/firecrawl-scrape.ts | 2 + .../src/tools/definitions/firecrawl-search.ts | 2 + .../src/tools/definitions/web-search.ts | 56 +----------------- 7 files changed, 49 insertions(+), 77 deletions(-) diff --git a/apps/supercode-cli/server/package.json b/apps/supercode-cli/server/package.json index cc06e39..1c77a86 100644 --- a/apps/supercode-cli/server/package.json +++ b/apps/supercode-cli/server/package.json @@ -1,6 +1,6 @@ { "name": "supercode-cli", - "version": "0.1.32", + "version": "0.1.33", "description": "AI-powered coding agent CLI", "main": "dist/main.js", "bin": { diff --git a/apps/supercode-cli/server/src/cli/ai/chat/chat.ts b/apps/supercode-cli/server/src/cli/ai/chat/chat.ts index 6535550..98957f6 100644 --- a/apps/supercode-cli/server/src/cli/ai/chat/chat.ts +++ b/apps/supercode-cli/server/src/cli/ai/chat/chat.ts @@ -43,6 +43,7 @@ import { buildSystemPrompt } from "src/cli/workspace/context.ts" import { tools } from "src/tools/registry.ts" import { setDelegateRuntime } from "src/tools/definitions/delegate.ts" import { CitationTracker } from "src/lib/citation-tracker.ts" +import { loadEnvOnce } from "src/lib/load-env" import { renderWorkspaceBanner } from "src/cli/workspace/format.ts" import { handleSlashCommand, isSlashCommand, COMMANDS } from "src/cli/commands/slashCommands/index.ts" import { @@ -276,6 +277,9 @@ async function streamAIResponse( if (workspaceInfo) { toolsToUse = { ...tools } + // Ensure .env vars are loaded (Bun only auto-loads .env from CWD, which + // may not be the server directory when launched from elsewhere). + loadEnvOnce() // When Firecrawl is configured, remove the legacy web_search tool so the // model reliably uses firecrawl_search instead of falling back to Google CSE. if (process.env.FIRECRAWL_API_KEY) { diff --git a/apps/supercode-cli/server/src/lib/load-env.ts b/apps/supercode-cli/server/src/lib/load-env.ts index 718e794..fa60ffc 100644 --- a/apps/supercode-cli/server/src/lib/load-env.ts +++ b/apps/supercode-cli/server/src/lib/load-env.ts @@ -1,33 +1,49 @@ import { readFileSync, existsSync } from "fs" -import { resolve, dirname } from "path" -import { fileURLToPath } from "url" +import { resolve } from "path" + +let _loaded = false function loadEnvFile(envPath: string) { if (!existsSync(envPath)) return false - const env = readFileSync(envPath, "utf-8") - for (const line of env.split("\n")) { + const raw = readFileSync(envPath, "utf8") + for (const line of raw.split(/\r?\n/)) { const trimmed = line.trim() if (!trimmed || trimmed.startsWith("#")) continue - const eqIdx = trimmed.indexOf("=") - if (eqIdx === -1) continue - const key = trimmed.slice(0, eqIdx).trim() - let value = trimmed.slice(eqIdx + 1).trim() - if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1) - } - if (!process.env[key]) { - process.env[key] = value + const eq = trimmed.indexOf("=") + if (eq <= 0) continue + const key = trimmed.slice(0, eq).trim() + if (process.env[key] !== undefined) continue + let val = trimmed.slice(eq + 1).trim() + if ( + (val.startsWith('"') && val.endsWith('"')) || + (val.startsWith("'") && val.endsWith("'")) + ) { + val = val.slice(1, -1) } + process.env[key] = val } return true } -// Try CWD .env first, then file-relative paths for bundled/global install -const __dirname = dirname(fileURLToPath(import.meta.url)) -const cwdEnv = resolve(process.cwd(), ".env") -const pkgEnv = resolve(__dirname, "../../.env") // dev: src/lib/ -> server/.env -const distEnv = resolve(__dirname, "../.env") // prod: dist/ -> server/.env +export function loadEnvOnce() { + if (_loaded) return + _loaded = true + + const candidates = [ + resolve(process.cwd(), ".env"), + resolve(process.cwd(), "..", ".env"), + resolve(process.cwd(), "..", "..", ".env"), + ] + let dir = process.cwd() + for (let i = 0; i < 5; i++) { + candidates.push(resolve(dir, ".env")) + dir = resolve(dir, "..") + } -try { - loadEnvFile(cwdEnv) || loadEnvFile(pkgEnv) || loadEnvFile(distEnv) -} catch {} + const seen = new Set() + for (const path of candidates) { + if (seen.has(path)) continue + seen.add(path) + if (loadEnvFile(path)) break + } +} diff --git a/apps/supercode-cli/server/src/tools/definitions/firecrawl-map.ts b/apps/supercode-cli/server/src/tools/definitions/firecrawl-map.ts index 62b34eb..443b03a 100644 --- a/apps/supercode-cli/server/src/tools/definitions/firecrawl-map.ts +++ b/apps/supercode-cli/server/src/tools/definitions/firecrawl-map.ts @@ -1,4 +1,5 @@ import { z } from "zod" +import { loadEnvOnce } from "../../lib/load-env" const FIRECRAWL_BASE = "https://api.firecrawl.dev/v2" @@ -24,6 +25,7 @@ export const firecrawlMapTool = { "If success is false, do NOT invent results — relay the error to the user.", parameters: firecrawlMapSchema, execute: async ({ url, search, limit, includeSubdomains }: FirecrawlMapArgs): Promise => { + loadEnvOnce() const apiKey = process.env.FIRECRAWL_API_KEY if (!apiKey) { diff --git a/apps/supercode-cli/server/src/tools/definitions/firecrawl-scrape.ts b/apps/supercode-cli/server/src/tools/definitions/firecrawl-scrape.ts index 82594ec..41ad58b 100644 --- a/apps/supercode-cli/server/src/tools/definitions/firecrawl-scrape.ts +++ b/apps/supercode-cli/server/src/tools/definitions/firecrawl-scrape.ts @@ -1,4 +1,5 @@ import { z } from "zod" +import { loadEnvOnce } from "../../lib/load-env" const FIRECRAWL_BASE = "https://api.firecrawl.dev/v2" @@ -22,6 +23,7 @@ export const firecrawlScrapeTool = { "If success is false, do NOT invent content — relay the error to the user and try a different approach.", parameters: firecrawlScrapeSchema, execute: async ({ url, maxChars }: FirecrawlScrapeArgs): Promise => { + loadEnvOnce() const apiKey = process.env.FIRECRAWL_API_KEY if (!apiKey) { diff --git a/apps/supercode-cli/server/src/tools/definitions/firecrawl-search.ts b/apps/supercode-cli/server/src/tools/definitions/firecrawl-search.ts index 64dec94..3db528d 100644 --- a/apps/supercode-cli/server/src/tools/definitions/firecrawl-search.ts +++ b/apps/supercode-cli/server/src/tools/definitions/firecrawl-search.ts @@ -1,4 +1,5 @@ import { z } from "zod" +import { loadEnvOnce } from "../../lib/load-env" const FIRECRAWL_BASE = "https://api.firecrawl.dev/v2" @@ -32,6 +33,7 @@ export const firecrawlSearchTool = { "If success is false, do NOT invent search results — relay the error to the user.", parameters: firecrawlSearchSchema, execute: async ({ query, maxResults, includeDomains, excludeDomains }: FirecrawlSearchArgs): Promise => { + loadEnvOnce() const apiKey = process.env.FIRECRAWL_API_KEY if (!apiKey) { diff --git a/apps/supercode-cli/server/src/tools/definitions/web-search.ts b/apps/supercode-cli/server/src/tools/definitions/web-search.ts index 883bb7f..b74f201 100644 --- a/apps/supercode-cli/server/src/tools/definitions/web-search.ts +++ b/apps/supercode-cli/server/src/tools/definitions/web-search.ts @@ -1,6 +1,5 @@ import { z } from "zod" -import { existsSync, readFileSync } from "node:fs" -import { resolve } from "node:path" +import { loadEnvOnce } from "../../lib/load-env" const webSearchSchema = z.object({ query: z.string().describe("Search query"), @@ -13,59 +12,6 @@ export type WebSearchResult = | { success: true; query: string; results: Array<{ title: string; snippet: string; link: string }> } | { success: false; error: string; hint?: string; configured: boolean } -// Load .env files from a few likely locations into process.env, but only for -// the keys we actually need (avoids stomping on anything else). -// -// Many users configure GOOGLE_API_KEY / GOOGLE_CSE_ID in apps/supercode-cli/ -// server/.env but process.env doesn't see them because Bun's auto-load only -// runs in entrypoints, not in lazily-loaded tool modules. This makes the tool -// behave as if it's "unconfigured" when it actually isn't. -function loadEnvOnce() { - if ((loadEnvOnce as any).__done) return - ;(loadEnvOnce as any).__done = true - - const candidates = [ - resolve(process.cwd(), ".env"), - resolve(process.cwd(), "..", ".env"), - resolve(process.cwd(), "..", "..", ".env"), - ] - // Walk up to find the server .env (works for both `bun src/index.ts` and - // `bun src/cli/main.ts` invocations from inside server/). - let dir = process.cwd() - for (let i = 0; i < 5; i++) { - candidates.push(resolve(dir, ".env")) - dir = resolve(dir, "..") - } - - const seen = new Set() - for (const path of candidates) { - if (seen.has(path)) continue - seen.add(path) - if (!existsSync(path)) continue - try { - const raw = readFileSync(path, "utf8") - for (const line of raw.split(/\r?\n/)) { - const trimmed = line.trim() - if (!trimmed || trimmed.startsWith("#")) continue - const eq = trimmed.indexOf("=") - if (eq <= 0) continue - const key = trimmed.slice(0, eq).trim() - if (process.env[key] !== undefined) continue - let val = trimmed.slice(eq + 1).trim() - if ( - (val.startsWith('"') && val.endsWith('"')) || - (val.startsWith("'") && val.endsWith("'")) - ) { - val = val.slice(1, -1) - } - process.env[key] = val - } - } catch { - // ignore unreadable .env files - } - } -} - export const webSearchTool = { description: "[LEGACY] Search the web using Google Custom Search. Consider using firecrawl_search instead — " +