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 — " +