diff --git a/apps/supercode-cli/server/package.json b/apps/supercode-cli/server/package.json index 1c77a86..d2fed9d 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.33", + "version": "0.1.34", "description": "AI-powered coding agent CLI", "main": "dist/main.js", "bin": { diff --git a/apps/supercode-cli/server/src/index.ts b/apps/supercode-cli/server/src/index.ts index 4b1e9a7..3b36eb4 100644 --- a/apps/supercode-cli/server/src/index.ts +++ b/apps/supercode-cli/server/src/index.ts @@ -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}`) }) diff --git a/apps/supercode-cli/server/src/lib/load-env.ts b/apps/supercode-cli/server/src/lib/load-env.ts index fa60ffc..b2f3bea 100644 --- a/apps/supercode-cli/server/src/lib/load-env.ts +++ b/apps/supercode-cli/server/src/lib/load-env.ts @@ -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 @@ -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() for (const path of candidates) { if (seen.has(path)) continue diff --git a/apps/supercode-cli/server/src/lib/proxy-tools.ts b/apps/supercode-cli/server/src/lib/proxy-tools.ts new file mode 100644 index 0000000..6b7a870 --- /dev/null +++ b/apps/supercode-cli/server/src/lib/proxy-tools.ts @@ -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, +): 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" } + } +} 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 443b03a..d4889c9 100644 --- a/apps/supercode-cli/server/src/tools/definitions/firecrawl-map.ts +++ b/apps/supercode-cli/server/src/tools/definitions/firecrawl-map.ts @@ -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" @@ -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 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 41ad58b..fd64bf3 100644 --- a/apps/supercode-cli/server/src/tools/definitions/firecrawl-scrape.ts +++ b/apps/supercode-cli/server/src/tools/definitions/firecrawl-scrape.ts @@ -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" @@ -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 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 3db528d..d2f1ebf 100644 --- a/apps/supercode-cli/server/src/tools/definitions/firecrawl-search.ts +++ b/apps/supercode-cli/server/src/tools/definitions/firecrawl-search.ts @@ -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" @@ -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. " + @@ -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 { 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 b74f201..ae99e21 100644 --- a/apps/supercode-cli/server/src/tools/definitions/web-search.ts +++ b/apps/supercode-cli/server/src/tools/definitions/web-search.ts @@ -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"), @@ -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.", @@ -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 {