Commit 9b89de7b7e51

Vincent Demeester <vincent@sbr.pm>
2026-02-04 16:19:13
pi/extensions: add search.ts for web search tool
Registers a web_search tool using the DuckDuckGo Instant Answer API. Provides a simple, no-API-key-required way to get quick web search results and summaries. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6727003
Changed files (1)
dots
pi
agent
extensions
dots/pi/agent/extensions/search.ts
@@ -0,0 +1,169 @@
+
+import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+import { Type } from "@sinclair/typebox";
+
+// Helper function to truncate text to avoid flooding the context
+function truncate(text: string, maxLength: number): string {
+  if (text.length <= maxLength) {
+    return text;
+  }
+  return text.slice(0, maxLength) + "...";
+}
+
+export default function (pi: ExtensionAPI) {
+  /**
+   * Web Search Tool (DuckDuckGo)
+   *
+   * This tool performs a web search using the DuckDuckGo Instant Answer API.
+   * It's a simple, no-API-key-required way to get quick summaries.
+   */
+  pi.registerTool({
+    name: "web_search",
+    label: "Web Search",
+    description: "Performs a web search using the DuckDuckGo API to answer questions or find information.",
+    parameters: Type.Object({
+      query: Type.String({ description: "The search query." }),
+    }),
+    async execute(toolCallId, params, signal) {
+      try {
+        const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(params.query)}&format=json&no_html=1`;
+        
+        const response = await fetch(url, { signal });
+        if (!response.ok) {
+          return { content: [{ type: "text", text: `Error: API request failed with status ${response.status}` }] };
+        }
+
+        const data = await response.json();
+        
+        // Extract the most useful information: Abstract, and first 3 related topics
+        const abstract = data.AbstractText ? `Abstract: ${data.AbstractText}` : "";
+        const results = (data.RelatedTopics || [])
+          .slice(0, 3)
+          .map((topic: any) => `- ${topic.Text}: ${topic.FirstURL}`)
+          .join("\n");
+        
+        const combined = `${abstract}\n\nRelated Results:\n${results}`;
+        
+        if (!combined.trim()) {
+           return { content: [{ type: "text", text: "No results found." }] };
+        }
+
+        return { content: [{ type: "text", text: truncate(combined, 4000) }] };
+
+      } catch (e: any) {
+        return { content: [{ type: "text", text: `An unexpected error occurred: ${e.message}` }] };
+      }
+    },
+  });
+
+  /**
+   * GitHub Code Search Tool
+   *
+   * This tool uses the GitHub API to search for code within repositories.
+   */
+  pi.registerTool({
+    name: "github_search",
+    label: "GitHub Code Search",
+    description: "Searches for code snippets on GitHub. You can specify a repository with 'repo:owner/repo'.",
+    parameters: Type.Object({
+      query: Type.String({ description: "The code search query. Examples: 'readFile repo:owner/project', 'my_function language:python'" }),
+    }),
+    async execute(toolCallId, params, signal) {
+      try {
+        // Delegate token retrieval to an external command.
+        const tokenResult = await pi.exec("gh", ["auth", "token"], { signal });
+        const token = tokenResult.stdout.trim();
+
+        if (tokenResult.code !== 0 || !token) {
+          return {
+            content: [{
+              type: "text",
+              text: `Error fetching GitHub token. Is 'passage' installed and is 'github.com/token' set?\nExit Code: ${tokenResult.code}\nStderr: ${tokenResult.stderr || '(empty)'}`
+            }]
+          };
+        }
+
+        const url = `https://api.github.com/search/code?q=${encodeURIComponent(params.query)}`;
+        
+        const response = await fetch(url, {
+          headers: {
+            "Accept": "application/vnd.github.v3+json",
+            "Authorization": `Bearer ${token}`,
+          },
+          signal,
+        });
+
+        if (response.status === 401) {
+            return { content: [{ type: "text", text: "Error: GitHub API request failed with status 401 (Unauthorized). The token provided by 'passage' may be invalid or expired." }] };
+        }
+        if (!response.ok) {
+          return { content: [{ type: "text", text: `Error: GitHub API request failed with status ${response.status}` }] };
+        }
+
+        const data = await response.json();
+        const items = data.items || [];
+
+        if (items.length === 0) {
+          return { content: [{ type: "text", text: "No code results found on GitHub." }] };
+        }
+
+        // Format the first 5 results
+        const results = items
+          .slice(0, 5)
+          .map((item: any) => `Path: ${item.path} (Repo: ${item.repository.full_name})\nURL: ${item.html_url}`)
+          .join("\n---\n");
+        
+        return { content: [{ type: "text", text: truncate(results, 4000) }] };
+
+      } catch (e: any) {
+        // Handle errors from pi.exec (e.g., command not found)
+        if (e.name === 'ExecError') {
+            return { content: [{ type: 'text', text: `Failed to execute token command: ${e.message}` }] };
+        }
+        return { content: [{ type: "text", text: `An unexpected error occurred: ${e.message}` }] };
+      }
+    },
+  });
+
+  /**
+   * Stack Overflow Search Tool
+   *
+   * This tool searches for questions on Stack Overflow.
+   */
+  pi.registerTool({
+    name: "stack_overflow_search",
+    label: "Stack Overflow Search",
+    description: "Searches for questions on Stack Overflow.",
+    parameters: Type.Object({
+      query: Type.String({ description: "The question to search for." }),
+    }),
+    async execute(toolCallId, params, signal) {
+      try {
+        const url = `https://api.stackexchange.com/2.3/search?order=desc&sort=relevance&site=stackoverflow&intitle=${encodeURIComponent(params.query)}`;
+        
+        const response = await fetch(url, { signal });
+        if (!response.ok) {
+          return { content: [{ type: "text", text: `Error: Stack Exchange API request failed with status ${response.status}` }] };
+        }
+
+        const data = await response.json();
+        const items = data.items || [];
+
+        if (items.length === 0) {
+          return { content: [{ type: "text", text: "No questions found on Stack Overflow." }] };
+        }
+
+        // Format the first 5 results
+        const results = items
+          .slice(0, 5)
+          .map((item: any) => `[Score: ${item.score}] ${item.is_answered ? "(Answered)" : ""} ${item.title}\nURL: ${item.link}`)
+          .join("\n---\n");
+        
+        return { content: [{ type: "text", text: truncate(results, 4000) }] };
+
+      } catch (e: any) {
+        return { content: [{ type: "text", text: `An unexpected error occurred: ${e.message}` }] };
+      }
+    },
+  });
+}