Commit 442f3998599f

Vincent Demeester <vincent@sbr.pm>
2026-04-23 15:55:45
feat(pi): add Brave Search API as primary backend
Added BraveAPIBackend to the search extension with automatic fallback to SearXNG when unavailable. Provisioned BRAVE_API_KEY via pass-run in the pir wrapper script.
1 parent a86d93b
Changed files (4)
dots
pi
agent
extensions
pkgs
my
dots/pi/agent/extensions/search/backends.ts
@@ -17,9 +17,52 @@ export interface SearchBackend {
   isAvailable(): Promise<boolean>;
 }
 
+/**
+ * Brave Search API backend - uses the Brave Search API with an API key.
+ * Primary backend when BRAVE_API_KEY is set.
+ * Free tier: 2,000 queries/month. See https://brave.com/search/api/
+ */
+export class BraveAPIBackend implements SearchBackend {
+  name = "Brave API";
+  private apiKey: string;
+
+  constructor(apiKey: string) {
+    this.apiKey = apiKey;
+  }
+
+  async isAvailable(): Promise<boolean> {
+    return !!this.apiKey;
+  }
+
+  async search(query: string, maxResults: number, signal?: AbortSignal): Promise<SearchResult[]> {
+    const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${maxResults}`;
+    const response = await fetch(url, {
+      headers: {
+        "Accept": "application/json",
+        "Accept-Encoding": "gzip",
+        "X-Subscription-Token": this.apiKey,
+      },
+      signal,
+    });
+
+    if (!response.ok) {
+      throw new Error(`Brave API request failed with status ${response.status}`);
+    }
+
+    const data = await response.json();
+    const results: SearchResult[] = (data.web?.results || []).slice(0, maxResults).map((r: any) => ({
+      title: r.title || "(no title)",
+      url: r.url || "",
+      snippet: r.description || "",
+    }));
+
+    return results;
+  }
+}
+
 /**
  * SearXNG backend - queries a self-hosted SearXNG instance via JSON API.
- * Primary backend for unlimited private searches.
+ * Fallback backend for unlimited private searches.
  */
 export class SearXNGBackend implements SearchBackend {
   name = "SearXNG";
dots/pi/agent/extensions/search/index.ts
@@ -3,24 +3,27 @@
  *
  * Provides web search and GitHub code search tools.
  * Web search uses multiple backends with automatic fallback:
- *   1. SearXNG (self-hosted, primary)
- *   2. ddgr CLI (DuckDuckGo via CLI)
- *   3. Playwright/Bing (headless Chrome browser search fallback)
+ *   1. Brave Search API (primary, requires BRAVE_API_KEY)
+ *   2. SearXNG (self-hosted fallback)
+ *   3. ddgr CLI (DuckDuckGo via CLI)
  *   4. DuckDuckGo Instant Answer API (always available, limited results)
  *
  * Additional backends available via /search-backend:
+ *   - bing: Playwright/Bing (headless Chrome browser search)
  *   - brave: Playwright/Brave Search (rate-limited after a few queries)
  *   - mojeek: Playwright/Mojeek (independent engine, no CAPTCHA)
  *   - ecosia: Playwright/Ecosia (Bing-powered, good results)
  *
  * Configuration via environment variables:
- *   SEARXNG_URL - SearXNG instance URL (default: https://search.sbr.pm)
+ *   BRAVE_API_KEY - Brave Search API key (enables brave-api backend)
+ *   SEARXNG_URL  - SearXNG instance URL (default: https://search.sbr.pm)
  */
 
 import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
 import { Text } from "@mariozechner/pi-tui";
 import { Type } from "@sinclair/typebox";
 import {
+  BraveAPIBackend,
   SearXNGBackend,
   DuckDuckGoBackend,
   DdgrBackend,
@@ -42,7 +45,10 @@ export default function (pi: ExtensionAPI) {
   const extensionDir = __dirname;
   const execFn = (cmd: string, args: string[], opts?: any) => pi.exec(cmd, args, opts);
 
+  const braveApiKey = process.env.BRAVE_API_KEY || "";
+
   const allBackends: Record<string, SearchBackend> = {
+    "brave-api": new BraveAPIBackend(braveApiKey),
     searxng: new SearXNGBackend(searxngUrl),
     ddgr: new DdgrBackend(execFn),
     bing: new PlaywrightBackend("bing", execFn, extensionDir),
@@ -55,7 +61,10 @@ export default function (pi: ExtensionAPI) {
   // Active backends in priority order
   // SearXNG first, then Playwright browsers (Bing most reliable, then Mojeek/Ecosia),
   // then ddgr CLI, then DDG API as last resort. Brave available via /search-backend.
-  let activeBackendNames = ["searxng", "bing", "mojeek", "ecosia", "ddgr", "duckduckgo"];
+  // Brave API first (fast, high quality), then SearXNG fallback, then browser-based fallbacks
+  let activeBackendNames = braveApiKey
+    ? ["brave-api", "searxng", "ddgr", "duckduckgo"]
+    : ["searxng", "bing", "mojeek", "ecosia", "ddgr", "duckduckgo"];
 
   // Helper to get current active backends
   const getActiveBackends = (): SearchBackend[] =>
pkgs/my/scripts/bin/pir
@@ -8,4 +8,5 @@ exec pass-run -q \
   -e GOOGLE_CLOUD_LOCATION=redhat/google/osp/location \
   -e GEMINI_API_KEY=redhat/google/osp/vdeemest-api-key \
   -e SYNTHETIC_API_KEY=ai/synthetic.new/api_key \
+  -e BRAVE_API_KEY=ai/brave/api_key \
   -- pi "$@"
pkgs/my/scripts/default.nix
@@ -5,7 +5,7 @@
 
 stdenv.mkDerivation {
   pname = "vde-scripts";
-  version = "0.12";
+  version = "0.13";
 
   src = ./.;