Skip to content

Commit 8a03ffb

Browse files
authored
Merge pull request #169 from yashdev9274/supercode-cli
feat(cli): add Exa search/fetch as alternative web search provider
2 parents 98737b1 + c109046 commit 8a03ffb

13 files changed

Lines changed: 293 additions & 10 deletions

File tree

apps/supercode-cli/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "supercode-cli",
3-
"version": "0.1.40",
3+
"version": "0.1.41",
44
"description": "AI-powered coding agent CLI",
55
"main": "dist/main.js",
66
"bin": {

apps/supercode-cli/server/src/agent/built-in.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ const planInfo: AgentInfo = info({
5858
{ permission: "firecrawl_search", pattern: "*", action: "allow" },
5959
{ permission: "firecrawl_scrape", pattern: "*", action: "allow" },
6060
{ permission: "firecrawl_map", pattern: "*", action: "allow" },
61+
{ permission: "exa_search", pattern: "*", action: "allow" },
62+
{ permission: "exa_fetch", pattern: "*", action: "allow" },
6163
{ permission: "read_instructions", pattern: "*", action: "allow" },
6264
{ permission: "task", pattern: "*", action: "allow" },
6365
],
@@ -109,6 +111,8 @@ const exploreInfo: AgentInfo = info({
109111
{ permission: "firecrawl_search", pattern: "*", action: "allow" },
110112
{ permission: "firecrawl_scrape", pattern: "*", action: "allow" },
111113
{ permission: "firecrawl_map", pattern: "*", action: "allow" },
114+
{ permission: "exa_search", pattern: "*", action: "allow" },
115+
{ permission: "exa_fetch", pattern: "*", action: "allow" },
112116
{ permission: "task", pattern: "*", action: "allow" },
113117
],
114118
prompt: "explore",

apps/supercode-cli/server/src/agent/runner.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const READ_TOOLS = new Set([
2727
"firecrawl_search",
2828
"firecrawl_scrape",
2929
"firecrawl_map",
30+
"exa_search",
31+
"exa_fetch",
3032
])
3133

3234
export async function runAgent(

apps/supercode-cli/server/src/cli/ai/chat/chat.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,10 +280,18 @@ async function streamAIResponse(
280280
// Ensure .env vars are loaded (Bun only auto-loads .env from CWD, which
281281
// may not be the server directory when launched from elsewhere).
282282
loadEnvOnce()
283-
// When Firecrawl is configured, remove the legacy web_search tool so the
284-
// model reliably uses firecrawl_search instead of falling back to Google CSE.
283+
// Priority: Firecrawl > Exa > legacy web_search.
284+
// When Firecrawl is configured, use firecrawl tools exclusively.
285285
if (process.env.FIRECRAWL_API_KEY) {
286286
delete (toolsToUse as Record<string, unknown>).web_search
287+
delete (toolsToUse as Record<string, unknown>).exa_search
288+
delete (toolsToUse as Record<string, unknown>).exa_fetch
289+
} else if (process.env.EXA_API_KEY) {
290+
// When Exa is configured (without Firecrawl), use exa tools and hide legacy.
291+
delete (toolsToUse as Record<string, unknown>).web_search
292+
delete (toolsToUse as Record<string, unknown>).firecrawl_search
293+
delete (toolsToUse as Record<string, unknown>).firecrawl_scrape
294+
delete (toolsToUse as Record<string, unknown>).firecrawl_map
287295
}
288296
// Wire the subagent runtime so the `delegate` tool can spawn focused subtasks.
289297
setDelegateRuntime({

apps/supercode-cli/server/src/cli/workspace/context.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,11 @@ export function buildSystemPrompt(info: WorkspaceInfo, hasTools = false): string
9191
lines.push("## Web Search Requirement")
9292
lines.push("")
9393
lines.push("When the user asks about a company, product, service, topic, or any information")
94-
lines.push("that may have changed since your training data, you MUST call `firecrawl_search`")
95-
lines.push("to retrieve current information. Do NOT answer from your training data — always")
96-
lines.push("search first. If `firecrawl_search` returns an error, tell the user search is")
97-
lines.push("unavailable. Never fabricate search results.")
94+
lines.push("that may have changed since your training data, you MUST call the available")
95+
lines.push("web search tool (`firecrawl_search` or `exa_search`) to retrieve current")
96+
lines.push("information. Do NOT answer from your training data — always search first.")
97+
lines.push("If search returns an error, tell the user search is unavailable.")
98+
lines.push("Never fabricate search results.")
9899
lines.push("")
99100
lines.push("## Tone and Style")
100101
lines.push("")
@@ -129,8 +130,8 @@ export function buildSystemPrompt(info: WorkspaceInfo, hasTools = false): string
129130
lines.push("")
130131
lines.push("This is non-negotiable:")
131132
lines.push("")
132-
lines.push("- Every `url_fetch`, `firecrawl_search`, `read_file`, `search_files`, and")
133-
lines.push(" `read_instructions` call returns a STRUCTURED envelope. Inspect it:")
133+
lines.push("- Every `url_fetch`, web search (`firecrawl_search`/`exa_search`), `read_file`, `search_files`, and")
134+
lines.push(" `read_instructions` call returns a STRUCTURED envelope. Inspect it:")
134135
lines.push(" `{ success: true, content: \"...\" }` means the tool worked and returned content.")
135136
lines.push(" `{ success: false, error: \"...\", hint: \"...\" }` means the tool failed.")
136137
lines.push("")

apps/supercode-cli/server/src/config/agent-config.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import { runCommandTool } from "../tools/definitions/run-command"
66
import { firecrawlSearchTool } from "../tools/definitions/firecrawl-search"
77
import { firecrawlScrapeTool } from "../tools/definitions/firecrawl-scrape"
88
import { firecrawlMapTool } from "../tools/definitions/firecrawl-map"
9+
import { exaSearchTool } from "../tools/definitions/exa-search"
10+
import { exaFetchTool } from "../tools/definitions/exa-fetch"
911

1012
const agentInstructions = `You are a full-stack coding agent that creates complete, production-ready applications.
1113
1214
YOUR WORKFLOW (follow exactly):
13-
1. RESEARCH — If you need docs, API references, or code examples, use firecrawl_search / firecrawl_scrape to fetch them first
15+
1. RESEARCH — If you need docs, API references, or code examples, use firecrawl_search (or exa_search) / firecrawl_scrape (or exa_fetch) to fetch them first
1416
2. PLAN — Decide the project structure, tech stack, and all files needed
1517
3. CREATE DIRS — Use run_command({ command: "mkdir -p <paths>" }) to create the directory structure
1618
4. WRITE FILES — Use write_file for each source file with complete, working code
@@ -60,6 +62,16 @@ export function createAppAgent(model: LanguageModel, systemPrompt?: string) {
6062
inputSchema: firecrawlMapTool.parameters,
6163
execute: async (input: any) => firecrawlMapTool.execute(input),
6264
}),
65+
exa_search: tool({
66+
description: exaSearchTool.description,
67+
inputSchema: exaSearchTool.parameters,
68+
execute: async (input: any) => exaSearchTool.execute(input),
69+
}),
70+
exa_fetch: tool({
71+
description: exaFetchTool.description,
72+
inputSchema: exaFetchTool.parameters,
73+
execute: async (input: any) => exaFetchTool.execute(input),
74+
}),
6375
},
6476
stopWhen: stepCountIs(50),
6577
})

apps/supercode-cli/server/src/config/tools.config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ export const availableTools: ToolConfig[] = [
5858
getTool: () => registryTools.firecrawl_map as unknown as Record<string, unknown>,
5959
enabled: false,
6060
},
61+
{
62+
id: "exa_search",
63+
name: "Exa Search",
64+
description:
65+
"Search the web using Exa. Returns relevant results with titles, snippets, and URLs",
66+
getTool: () => registryTools.exa_search as unknown as Record<string, unknown>,
67+
enabled: false,
68+
},
69+
{
70+
id: "exa_fetch",
71+
name: "Exa Fetch",
72+
description:
73+
"Fetch and extract full text content from a URL using Exa. Handles JS-rendered pages",
74+
getTool: () => registryTools.exa_fetch as unknown as Record<string, unknown>,
75+
enabled: false,
76+
},
6177
{
6278
id: "write_file",
6379
name: "Write File",

apps/supercode-cli/server/src/index.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,7 @@ app.put("/api/conversations/:id/title", async (req, res) => {
751751
// ── Tool proxy endpoints (use server-side API keys) ──
752752

753753
const FIRECRAWL_BASE = "https://api.firecrawl.dev/v2"
754+
const EXA_BASE = "https://api.exa.ai"
754755

755756
app.post("/api/tools/firecrawl-search", async (req, res) => {
756757
try {
@@ -815,6 +816,48 @@ app.post("/api/tools/firecrawl-map", async (req, res) => {
815816
}
816817
})
817818

819+
app.post("/api/tools/exa-search", async (req, res) => {
820+
try {
821+
const user = await getUserFromBearer(req)
822+
if (!user) { res.status(401).json({ error: "Unauthorized" }); return }
823+
824+
const apiKey = process.env.EXA_API_KEY
825+
if (!apiKey) { res.status(500).json({ error: "Exa search not configured on server" }); return }
826+
827+
const response = await fetch(`${EXA_BASE}/search`, {
828+
method: "POST",
829+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
830+
body: JSON.stringify(req.body),
831+
signal: AbortSignal.timeout(30000),
832+
})
833+
const data = await response.json()
834+
res.status(response.status).json(data)
835+
} catch (error: any) {
836+
res.status(500).json({ error: error.message || "Exa search proxy failed" })
837+
}
838+
})
839+
840+
app.post("/api/tools/exa-fetch", async (req, res) => {
841+
try {
842+
const user = await getUserFromBearer(req)
843+
if (!user) { res.status(401).json({ error: "Unauthorized" }); return }
844+
845+
const apiKey = process.env.EXA_API_KEY
846+
if (!apiKey) { res.status(500).json({ error: "Exa fetch not configured on server" }); return }
847+
848+
const response = await fetch(`${EXA_BASE}/contents`, {
849+
method: "POST",
850+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
851+
body: JSON.stringify(req.body),
852+
signal: AbortSignal.timeout(30000),
853+
})
854+
const data = await response.json()
855+
res.status(response.status).json(data)
856+
} catch (error: any) {
857+
res.status(500).json({ error: error.message || "Exa fetch proxy failed" })
858+
}
859+
})
860+
818861
app.post("/api/tools/web-search", async (req, res) => {
819862
try {
820863
const user = await getUserFromBearer(req)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { loadEnvOnce } from "./load-env"
2+
import { proxyToolCall } from "./proxy-tools"
3+
4+
const EXA_BASE = "https://api.exa.ai"
5+
6+
export interface ExaOptions {
7+
apiPath: string
8+
proxyAction: string
9+
body: Record<string, unknown>
10+
timeout?: number
11+
}
12+
13+
export interface ExaResult {
14+
ok: boolean
15+
data?: any
16+
error?: string
17+
hint?: string
18+
status?: number
19+
}
20+
21+
function statusHint(status: number): string | undefined {
22+
if (status === 429) return "Rate limited. Try again later."
23+
if (status === 402) return "Exa account requires payment. Check your credit balance."
24+
if (status === 401 || status === 403) return "Invalid EXA_API_KEY. Check your API key."
25+
return undefined
26+
}
27+
28+
export async function exaFetch({
29+
apiPath,
30+
proxyAction,
31+
body,
32+
timeout = 30000,
33+
}: ExaOptions): Promise<ExaResult> {
34+
loadEnvOnce()
35+
const apiKey = process.env.EXA_API_KEY
36+
37+
if (!apiKey) {
38+
const proxy = await proxyToolCall(`/api/tools/${proxyAction}`, body)
39+
if (proxy.ok) return { ok: true, data: proxy.data }
40+
return {
41+
ok: false,
42+
error: `Exa proxy failed: ${proxy.error}. Set EXA_API_KEY environment variable locally, or fix the proxy issue above.`,
43+
hint: `Proxy error: ${proxy.error}. Try url_fetch as a fallback.`,
44+
}
45+
}
46+
47+
try {
48+
const res = await fetch(`${EXA_BASE}${apiPath}`, {
49+
method: "POST",
50+
headers: {
51+
"x-api-key": apiKey,
52+
"Content-Type": "application/json",
53+
},
54+
body: JSON.stringify(body),
55+
signal: AbortSignal.timeout(timeout),
56+
})
57+
58+
const data = await res.json()
59+
60+
if (!res.ok) {
61+
return {
62+
ok: false,
63+
error: `Exa returned HTTP ${res.status}`,
64+
hint: statusHint(res.status),
65+
status: res.status,
66+
}
67+
}
68+
69+
return { ok: true, data }
70+
} catch (err: any) {
71+
const isTimeout = err?.name === "TimeoutError" || err?.name === "AbortError"
72+
return {
73+
ok: false,
74+
error: isTimeout ? "Request timed out" : (err.message || String(err)),
75+
hint: isTimeout ? "Exa API may be slow or unreachable. Try url_fetch instead." : undefined,
76+
}
77+
}
78+
}

apps/supercode-cli/server/src/tools/definitions/delegate.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ const READ_ONLY_TOOLS = new Set([
4848
"firecrawl_search",
4949
"firecrawl_scrape",
5050
"firecrawl_map",
51+
"exa_search",
52+
"exa_fetch",
5153
"task",
5254
])
5355

0 commit comments

Comments
 (0)