Commit 9b89de7b7e51
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}` }] };
+ }
+ },
+ });
+}