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.33",
"version": "0.1.34",
"description": "AI-powered coding agent CLI",
"main": "dist/main.js",
"bin": {
Expand Down
86 changes: 86 additions & 0 deletions apps/supercode-cli/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,92 @@ app.put("/api/conversations/:id/title", async (req, res) => {
}
})

// ── Tool proxy endpoints (use server-side API keys) ──

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

app.post("/api/tools/firecrawl-search", async (req, res) => {
try {
const user = await getUserFromBearer(req)
if (!user) { res.status(401).json({ error: "Unauthorized" }); return }

const apiKey = process.env.FIRECRAWL_API_KEY
if (!apiKey) { res.status(500).json({ error: "Firecrawl not configured on server" }); return }

const response = await fetch(`${FIRECRAWL_BASE}/search`, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify(req.body),
signal: AbortSignal.timeout(30000),
})
const data = await response.json()
res.status(response.status).json(data)
} catch (error: any) {
res.status(500).json({ error: error.message || "Firecrawl search proxy failed" })
}
})

app.post("/api/tools/firecrawl-scrape", async (req, res) => {
try {
const user = await getUserFromBearer(req)
if (!user) { res.status(401).json({ error: "Unauthorized" }); return }

const apiKey = process.env.FIRECRAWL_API_KEY
if (!apiKey) { res.status(500).json({ error: "Firecrawl not configured on server" }); return }

const response = await fetch(`${FIRECRAWL_BASE}/scrape`, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify(req.body),
signal: AbortSignal.timeout(30000),
})
const data = await response.json()
res.status(response.status).json(data)
} catch (error: any) {
res.status(500).json({ error: error.message || "Firecrawl scrape proxy failed" })
}
})

app.post("/api/tools/firecrawl-map", async (req, res) => {
try {
const user = await getUserFromBearer(req)
if (!user) { res.status(401).json({ error: "Unauthorized" }); return }

const apiKey = process.env.FIRECRAWL_API_KEY
if (!apiKey) { res.status(500).json({ error: "Firecrawl not configured on server" }); return }

const response = await fetch(`${FIRECRAWL_BASE}/map`, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify(req.body),
signal: AbortSignal.timeout(60000),
})
const data = await response.json()
res.status(response.status).json(data)
} catch (error: any) {
res.status(500).json({ error: error.message || "Firecrawl map proxy failed" })
}
})

app.post("/api/tools/web-search", async (req, res) => {
try {
const user = await getUserFromBearer(req)
if (!user) { res.status(401).json({ error: "Unauthorized" }); return }

const apiKey = process.env.GOOGLE_API_KEY
const cx = process.env.GOOGLE_CSE_ID
if (!apiKey || !cx) { res.status(500).json({ error: "Google Custom Search not configured on server" }); return }

const { query, maxResults = 5 } = req.body
const url = `https://www.googleapis.com/customsearch/v1?key=${apiKey}&cx=${cx}&q=${encodeURIComponent(query)}`
const response = await fetch(url)
const data = await response.json()
res.status(response.status).json(data)
} catch (error: any) {
res.status(500).json({ error: error.message || "Web search proxy failed" })
}
})

app.listen(port, () => {
console.log(`Server is running on port ${port}`)
})
13 changes: 12 additions & 1 deletion apps/supercode-cli/server/src/lib/load-env.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { readFileSync, existsSync } from "fs"
import { resolve } from "path"
import { resolve, dirname } from "path"
import { fileURLToPath } from "url"

let _loaded = false

Expand Down Expand Up @@ -40,6 +41,16 @@ export function loadEnvOnce() {
dir = resolve(dir, "..")
}

// Also check relative to this module's location (handles bundled CLI
// running from any cwd — dist/ or src/lib/ both resolve to server/.env)
try {
const moduleDir = dirname(fileURLToPath(import.meta.url))
candidates.push(resolve(moduleDir, "..", "..", ".env"))
candidates.push(resolve(moduleDir, "..", ".env"))
} catch {
// import.meta.url unavailable outside ESM
}

const seen = new Set<string>()
for (const path of candidates) {
if (seen.has(path)) continue
Expand Down
32 changes: 32 additions & 0 deletions apps/supercode-cli/server/src/lib/proxy-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { getStoredToken } from "src/lib/token"

const BASE_URL = process.env.SUPERCODE_SERVER_URL || "https://supercode-8w7e.onrender.com"

export async function proxyToolCall(
endpoint: string,
body: Record<string, unknown>,
): Promise<{ ok: false; error: string } | { ok: true; data: any }> {
const token = await getStoredToken()
if (!token?.access_token) {
return { ok: false, error: "Not authenticated. Run 'supercode login' first." }
}

try {
const res = await fetch(`${BASE_URL}${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token.access_token}`,
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(60000),
})
const data = await res.json() as any
if (!res.ok) {
return { ok: false, error: data?.error || `Server proxy returned HTTP ${res.status}` }
}
return { ok: true, data }
} catch (error: any) {
return { ok: false, error: error.message || "Failed to reach server proxy" }
}
}
25 changes: 21 additions & 4 deletions apps/supercode-cli/server/src/tools/definitions/firecrawl-map.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from "zod"
import { loadEnvOnce } from "../../lib/load-env"
import { proxyToolCall } from "../../lib/proxy-tools"

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

Expand Down Expand Up @@ -29,17 +30,33 @@ export const firecrawlMapTool = {
const apiKey = process.env.FIRECRAWL_API_KEY

if (!apiKey) {
const result: FirecrawlMapResult = {
const proxy = await proxyToolCall("/api/tools/firecrawl-map", {
url,
search,
limit,
includeSubdomains,
})

if (proxy.ok) {
const links = Array.isArray(proxy.data?.links) ? proxy.data.links : []
return JSON.stringify({
success: true,
url,
links,
total: links.length,
} satisfies FirecrawlMapResult)
}

return JSON.stringify({
success: false,
error: "Firecrawl map is not configured. Set FIRECRAWL_API_KEY environment variable.",
hint:
"Tell the user firecrawl_map is unavailable and suggest alternatives: " +
"(1) use firecrawl_search with site:domain.com, " +
"(2) use the existing url_fetch tool, " +
"(3) set FIRECRAWL_API_KEY in the .env file.",
"(3) set FIRECRAWL_API_KEY in the .env or on the server (Render).",
configured: false,
}
return JSON.stringify(result)
} satisfies FirecrawlMapResult)
}

let urlObj: URL
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from "zod"
import { loadEnvOnce } from "../../lib/load-env"
import { proxyToolCall } from "../../lib/proxy-tools"

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

Expand Down Expand Up @@ -27,15 +28,31 @@ export const firecrawlScrapeTool = {
const apiKey = process.env.FIRECRAWL_API_KEY

if (!apiKey) {
const result: FirecrawlScrapeResult = {
success: false,
error: "Firecrawl scrape is not configured. Set FIRECRAWL_API_KEY environment variable.",
hint:
"Tell the user firecrawl_scrape is unavailable and suggest alternatives: " +
"(1) use the existing url_fetch tool, " +
"(2) set FIRECRAWL_API_KEY in the .env file.",
const proxy = await proxyToolCall("/api/tools/firecrawl-scrape", {
url,
formats: ["markdown"],
onlyMainContent: true,
})

if (proxy.ok) {
const markdown = proxy.data?.data?.markdown ?? ""
const cleaned = markdown.trim()
if (cleaned) {
return JSON.stringify({
success: true,
content: cleaned.slice(0, maxChars),
bytesRead: cleaned.length,
status: 200,
contentType: proxy.data?.data?.metadata?.contentType ?? "text/markdown",
} satisfies FirecrawlScrapeResult)
}
return JSON.stringify({
success: false,
error: "Fetched URL returned no extractable text content",
status: 200,
hint: "The page may be JavaScript-rendered or require authentication.",
} satisfies FirecrawlScrapeResult)
}
return JSON.stringify(result)
}

let urlObj: URL
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from "zod"
import { loadEnvOnce } from "../../lib/load-env"
import { proxyToolCall } from "../../lib/proxy-tools"

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

Expand All @@ -22,6 +23,14 @@ export type FirecrawlSearchResult =
| { success: true; query: string; results: Array<{ title: string; snippet: string; link: string }> }
| { success: false; error: string; hint?: string; configured: boolean }

function formatResults(items: any[]): Array<{ title: string; snippet: string; link: string }> {
return items.map((item: any) => ({
title: String(item.title ?? ""),
snippet: String(item.description ?? item.snippet ?? ""),
link: String(item.url ?? item.link ?? ""),
}))
}

export const firecrawlSearchTool = {
description:
"[REQUIRED] Search the web for any company, product, service, topic, or current information. " +
Expand All @@ -37,17 +46,31 @@ export const firecrawlSearchTool = {
const apiKey = process.env.FIRECRAWL_API_KEY

if (!apiKey) {
const result: FirecrawlSearchResult = {
const proxy = await proxyToolCall("/api/tools/firecrawl-search", {
query,
limit: maxResults,
sources: [{ type: "web" }],
...(includeDomains ? { includeDomains } : {}),
...(excludeDomains ? { excludeDomains } : {}),
})

if (proxy.ok) {
const webResults = Array.isArray(proxy.data?.data?.web) ? proxy.data.data.web : []
const newsResults = Array.isArray(proxy.data?.data?.news) ? proxy.data.data.news : []
const results = formatResults([...webResults, ...newsResults].slice(0, maxResults))
return JSON.stringify({ success: true, query, results } satisfies FirecrawlSearchResult)
}

return JSON.stringify({
success: false,
error: "Firecrawl search is not configured. Set FIRECRAWL_API_KEY environment variable.",
hint:
"Tell the user firecrawl_search is unavailable and suggest alternatives: " +
"(1) ask the user to provide a specific URL and call firecrawl_scrape on it, " +
"(2) use the existing web_search tool with Google CSE, " +
"(3) set FIRECRAWL_API_KEY in the .env file.",
"(3) set FIRECRAWL_API_KEY in the .env or on the server (Render).",
configured: false,
}
return JSON.stringify(result)
} satisfies FirecrawlSearchResult)
}

try {
Expand Down
23 changes: 19 additions & 4 deletions apps/supercode-cli/server/src/tools/definitions/web-search.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from "zod"
import { loadEnvOnce } from "../../lib/load-env"
import { proxyToolCall } from "../../lib/proxy-tools"

const webSearchSchema = z.object({
query: z.string().describe("Search query"),
Expand Down Expand Up @@ -28,7 +29,22 @@ export const webSearchTool = {
const cx = process.env.GOOGLE_CSE_ID

if (!apiKey || !cx) {
const result: WebSearchResult = {
const proxy = await proxyToolCall("/api/tools/web-search", {
query,
maxResults,
})

if (proxy.ok) {
const items = Array.isArray(proxy.data?.items) ? proxy.data.items : []
const results = items.slice(0, maxResults).map((item: any) => ({
title: String(item.title ?? ""),
snippet: String(item.snippet ?? ""),
link: String(item.link ?? ""),
}))
return JSON.stringify({ success: true, query, results } satisfies WebSearchResult)
}

return JSON.stringify({
success: false,
error:
"Web search is not configured. Set GOOGLE_API_KEY and GOOGLE_CSE_ID environment variables.",
Expand All @@ -37,10 +53,9 @@ export const webSearchTool = {
"(1) ask the user to provide a specific URL and call url_fetch on it, " +
"(2) call url_fetch on a known URL (e.g. api.github.com/repos/{owner}/{name} for GitHub, " +
"raw.githubusercontent.com/{owner}/{name}/main/README.md for raw files), " +
"(3) ask the user to enable web_search by setting the env vars in apps/supercode-cli/server/.env.",
"(3) set GOOGLE_API_KEY and GOOGLE_CSE_ID in the .env or on the server (Render).",
configured: false,
}
return JSON.stringify(result)
} satisfies WebSearchResult)
}

try {
Expand Down
Loading