Skip to content
Merged
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: 1 addition & 1 deletion apps/supercode-cli/server/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
4 changes: 4 additions & 0 deletions apps/supercode-cli/server/src/cli/ai/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
58 changes: 37 additions & 21 deletions apps/supercode-cli/server/src/lib/load-env.ts
Original file line number Diff line number Diff line change
@@ -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<string>()
for (const path of candidates) {
if (seen.has(path)) continue
seen.add(path)
if (loadEnvFile(path)) break
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod"
import { loadEnvOnce } from "../../lib/load-env"

const FIRECRAWL_BASE = "https://api.firecrawl.dev/v2"

Expand All @@ -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<string> => {
loadEnvOnce()
const apiKey = process.env.FIRECRAWL_API_KEY

if (!apiKey) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod"
import { loadEnvOnce } from "../../lib/load-env"

const FIRECRAWL_BASE = "https://api.firecrawl.dev/v2"

Expand All @@ -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<string> => {
loadEnvOnce()
const apiKey = process.env.FIRECRAWL_API_KEY

if (!apiKey) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod"
import { loadEnvOnce } from "../../lib/load-env"

const FIRECRAWL_BASE = "https://api.firecrawl.dev/v2"

Expand Down Expand Up @@ -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<string> => {
loadEnvOnce()
const apiKey = process.env.FIRECRAWL_API_KEY

if (!apiKey) {
Expand Down
56 changes: 1 addition & 55 deletions apps/supercode-cli/server/src/tools/definitions/web-search.ts
Original file line number Diff line number Diff line change
@@ -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"),
Expand All @@ -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<string>()
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 — " +
Expand Down
Loading