@alasano/pi-exa 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ import { defineTool } from '@earendil-works/pi-coding-agent';
2
+ import type { Static } from '@sinclair/typebox';
3
+ import { exaPost } from '../client';
4
+ import { formatSearchResponse } from '../format';
5
+ import { truncateToolOutput } from '../output';
6
+ import {
7
+ buildSearchPreview,
8
+ metadataFromArgs,
9
+ renderExaCall,
10
+ renderExaResult,
11
+ sendProgress,
12
+ } from '../render';
13
+ import { WebSearchParamsSchema } from '../schemas';
14
+ import type { ExaSearchResponse, ExaToolDetails, SearchCategory, SearchType } from '../types';
15
+ import { withExaApiKey, errorResult } from './helpers';
16
+
17
+ type WebSearchParams = Static<typeof WebSearchParamsSchema>;
18
+
19
+ export interface WebSearchRequest {
20
+ query: string;
21
+ type: SearchType;
22
+ numResults: number;
23
+ category?: SearchCategory;
24
+ contents: {
25
+ highlights: true;
26
+ };
27
+ }
28
+
29
+ const CATEGORY_PATTERN = /\bcategory:(company|research\s*paper|news|personal\s*site|people)\b/i;
30
+
31
+ function parseCategory(query: string): { query: string; category?: SearchCategory } {
32
+ const match = query.match(CATEGORY_PATTERN);
33
+ if (!match) return { query };
34
+
35
+ return {
36
+ query: query.replace(match[0], '').replace(/\s+/g, ' ').trim(),
37
+ category: match[1]?.toLowerCase().replace(/\s+/g, ' ') as SearchCategory,
38
+ };
39
+ }
40
+
41
+ export function buildWebSearchRequest(params: WebSearchParams): WebSearchRequest {
42
+ const parsed = parseCategory(params.query);
43
+ return {
44
+ query: parsed.query || params.query,
45
+ type: 'auto',
46
+ numResults: params.numResults ?? 10,
47
+ ...(parsed.category ? { category: parsed.category } : {}),
48
+ contents: {
49
+ highlights: true,
50
+ },
51
+ };
52
+ }
53
+
54
+ export function createWebSearchTool() {
55
+ return defineTool<
56
+ typeof WebSearchParamsSchema,
57
+ ExaToolDetails<ExaSearchResponse, WebSearchRequest> | undefined
58
+ >({
59
+ name: 'web_search_exa',
60
+ label: 'Exa Web Search',
61
+ description:
62
+ 'Search the web with Exa and return compact, highlight-first results. Use for broad web lookup before fetching full pages.',
63
+ promptSnippet: 'Search the web with compact Exa highlights',
64
+ promptGuidelines: [
65
+ 'Use web_search_exa for broad web lookup, then use web_fetch_exa only on selected URLs that need fuller content.',
66
+ 'Prefer web_search_exa results as source leads; do not paste raw search output when a concise synthesis is enough.',
67
+ ],
68
+ parameters: WebSearchParamsSchema,
69
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
70
+ const request = buildWebSearchRequest(params);
71
+ try {
72
+ return await withExaApiKey(ctx, async (apiKey) => {
73
+ sendProgress(onUpdate, 'Searching Exa...');
74
+ const response = await exaPost<ExaSearchResponse>(apiKey, '/search', request, signal);
75
+ const output = await truncateToolOutput(
76
+ formatSearchResponse(response),
77
+ 'web-search-exa',
78
+ 'Refine your query, reduce numResults, or use web_fetch_exa only for selected URLs. ',
79
+ );
80
+ return {
81
+ content: [{ type: 'text', text: output.text }],
82
+ details: {
83
+ endpoint: '/search',
84
+ request,
85
+ response,
86
+ requestId: response.requestId,
87
+ count: response.results?.length ?? 0,
88
+ costDollars: response.costDollars,
89
+ preview: buildSearchPreview(response, output),
90
+ truncated: output.truncation.truncated,
91
+ truncation: output.truncation,
92
+ fullOutputPath: output.fullOutputPath,
93
+ },
94
+ };
95
+ });
96
+ } catch (error) {
97
+ return errorResult(error);
98
+ }
99
+ },
100
+ renderCall(args, theme) {
101
+ return renderExaCall(
102
+ 'web_search_exa',
103
+ `"${args.query}"`,
104
+ theme,
105
+ metadataFromArgs(args, ['numResults']),
106
+ );
107
+ },
108
+ renderResult(result, options, theme) {
109
+ return renderExaResult(result, options, theme, 'Searching Exa...');
110
+ },
111
+ });
112
+ }
@@ -0,0 +1,238 @@
1
+ export type JsonObject = Record<string, unknown>;
2
+
3
+ export type SearchType = 'auto' | 'fast' | 'instant';
4
+
5
+ export type ExaLink = {
6
+ url?: string;
7
+ title?: string;
8
+ altText?: string;
9
+ };
10
+
11
+ export type ExaExtras = {
12
+ links?: Array<string | ExaLink>;
13
+ imageLinks?: Array<string | ExaLink>;
14
+ };
15
+
16
+ export type ExaGroundingCitation = {
17
+ url?: string;
18
+ title?: string;
19
+ };
20
+
21
+ export type ExaGrounding = {
22
+ field?: string;
23
+ citations?: ExaGroundingCitation[];
24
+ confidence?: string;
25
+ };
26
+
27
+ export type ExaSearchOutput = {
28
+ content?: string | JsonObject;
29
+ grounding?: ExaGrounding[];
30
+ };
31
+
32
+ export type SearchCategory =
33
+ | 'company'
34
+ | 'research paper'
35
+ | 'news'
36
+ | 'pdf'
37
+ | 'github'
38
+ | 'personal site'
39
+ | 'people'
40
+ | 'financial report';
41
+
42
+ export interface ExaSearchResult {
43
+ id?: string;
44
+ title?: string;
45
+ url?: string;
46
+ publishedDate?: string;
47
+ author?: string | null;
48
+ text?: string;
49
+ highlights?: string[];
50
+ summary?: string | JsonObject;
51
+ score?: number;
52
+ image?: string;
53
+ favicon?: string;
54
+ subpages?: ExaSearchResult[];
55
+ links?: Array<string | ExaLink>;
56
+ imageLinks?: Array<string | ExaLink>;
57
+ extras?: ExaExtras;
58
+ entities?: unknown[];
59
+ }
60
+
61
+ export interface ExaSearchResponse {
62
+ requestId?: string;
63
+ autopromptString?: string;
64
+ resolvedSearchType?: string;
65
+ searchType?: string | null;
66
+ searchTime?: number;
67
+ results?: ExaSearchResult[];
68
+ output?: ExaSearchOutput;
69
+ statuses?: ExaContentsStatus[];
70
+ context?: string;
71
+ autoDate?: string;
72
+ costDollars?: JsonObject;
73
+ }
74
+
75
+ export interface ExaContentsStatus {
76
+ id?: string;
77
+ url?: string;
78
+ status?: string;
79
+ error?:
80
+ | string
81
+ | {
82
+ tag?: string;
83
+ message?: string;
84
+ httpStatusCode?: number | null;
85
+ };
86
+ }
87
+
88
+ export interface ExaContentsResponse {
89
+ requestId?: string;
90
+ results?: ExaSearchResult[];
91
+ statuses?: ExaContentsStatus[];
92
+ costDollars?: JsonObject;
93
+ }
94
+
95
+ export interface ExaAnswerResponse {
96
+ requestId?: string;
97
+ answer?: string | JsonObject;
98
+ citations?: ExaSearchResult[];
99
+ costDollars?: JsonObject;
100
+ }
101
+
102
+ export type ExaAgentRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled';
103
+
104
+ export type ExaAgentStopReason = 'schema_satisfied' | 'budget_reached' | 'error' | 'cancelled';
105
+
106
+ export type ExaAgentEffort = 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'auto';
107
+
108
+ export type ExaAgentConfidence = 'low' | 'medium' | 'high';
109
+
110
+ export interface ExaAgentInput {
111
+ data?: JsonObject[];
112
+ exclusion?: JsonObject[];
113
+ }
114
+
115
+ export interface ExaAgentGroundingCitation {
116
+ url: string;
117
+ title?: string | null;
118
+ [key: string]: unknown;
119
+ }
120
+
121
+ export interface ExaAgentGroundingEntry {
122
+ field?: string;
123
+ citations?: ExaAgentGroundingCitation[];
124
+ score?: number | null;
125
+ confidence?: ExaAgentConfidence | null;
126
+ [key: string]: unknown;
127
+ }
128
+
129
+ export interface ExaAgentOutput {
130
+ text?: string | null;
131
+ structured?: unknown;
132
+ grounding?: ExaAgentGroundingEntry[] | null;
133
+ [key: string]: unknown;
134
+ }
135
+
136
+ export interface ExaAgentUsage {
137
+ agentComputeUnits?: number;
138
+ searches?: number;
139
+ emails?: number;
140
+ phoneNumbers?: number;
141
+ [key: string]: unknown;
142
+ }
143
+
144
+ export interface ExaAgentCostDollars {
145
+ total?: number;
146
+ agentCompute?: number;
147
+ search?: number;
148
+ emails?: number;
149
+ phoneNumbers?: number;
150
+ [key: string]: unknown;
151
+ }
152
+
153
+ export interface ExaAgentError {
154
+ type?: string;
155
+ code?: string;
156
+ message?: string;
157
+ path?: string;
158
+ keyword?: string;
159
+ expected?: unknown;
160
+ actual?: unknown;
161
+ [key: string]: unknown;
162
+ }
163
+
164
+ export interface ExaAgentRunRequest {
165
+ query?: string;
166
+ systemPrompt?: string;
167
+ input?: ExaAgentInput;
168
+ outputSchema?: JsonObject | null;
169
+ effort?: ExaAgentEffort;
170
+ previousRunId?: string;
171
+ metadata?: Record<string, string>;
172
+ [key: string]: unknown;
173
+ }
174
+
175
+ export interface ExaAgentRun {
176
+ id: string;
177
+ object?: string;
178
+ status: ExaAgentRunStatus;
179
+ stopReason?: ExaAgentStopReason | null;
180
+ createdAt?: string;
181
+ completedAt?: string | null;
182
+ request?: ExaAgentRunRequest | null;
183
+ output?: ExaAgentOutput | null;
184
+ usage?: ExaAgentUsage;
185
+ costDollars?: ExaAgentCostDollars;
186
+ error?: ExaAgentError;
187
+ [key: string]: unknown;
188
+ }
189
+
190
+ export interface ExaAgentEvent {
191
+ id?: string;
192
+ event: string;
193
+ data: JsonObject;
194
+ createdAt?: string;
195
+ [key: string]: unknown;
196
+ }
197
+
198
+ export interface ExaAgentRunListResponse {
199
+ object?: string;
200
+ data: ExaAgentRun[];
201
+ hasMore: boolean;
202
+ nextCursor: string | null;
203
+ }
204
+
205
+ export interface ExaAgentEventListResponse {
206
+ object?: string;
207
+ data: ExaAgentEvent[];
208
+ hasMore: boolean;
209
+ nextCursor: string | null;
210
+ }
211
+
212
+ export interface ExaDeletedAgentRun {
213
+ id: string;
214
+ object?: string;
215
+ deleted: boolean;
216
+ }
217
+
218
+ export interface PreviewDetails {
219
+ kind: 'search' | 'contents' | 'answer' | 'agent';
220
+ summary: string;
221
+ lines: string[];
222
+ expandedLines?: string[];
223
+ truncated?: boolean;
224
+ fullOutputPath?: string;
225
+ }
226
+
227
+ export interface ExaToolDetails<TResponse = unknown, TRequest = unknown> {
228
+ endpoint: string;
229
+ request: TRequest;
230
+ response: TResponse;
231
+ requestId?: string;
232
+ count?: number;
233
+ costDollars?: JsonObject;
234
+ preview?: PreviewDetails;
235
+ truncated?: boolean;
236
+ truncation?: unknown;
237
+ fullOutputPath?: string;
238
+ }
@@ -0,0 +1,26 @@
1
+ import type { JsonObject } from './types';
2
+
3
+ export function asString(value: unknown): string | undefined {
4
+ if (typeof value !== 'string') return undefined;
5
+ const trimmed = value.trim();
6
+ return trimmed ? trimmed : undefined;
7
+ }
8
+
9
+ export function isRecord(value: unknown): value is JsonObject {
10
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
11
+ }
12
+
13
+ export function compactObject<T extends Record<string, unknown>>(input: T): Partial<T> {
14
+ const output: Partial<T> = {};
15
+ for (const [key, value] of Object.entries(input)) {
16
+ if (value !== undefined) {
17
+ output[key as keyof T] = value as T[keyof T];
18
+ }
19
+ }
20
+ return output;
21
+ }
22
+
23
+ export function truncateText(text: string, maxCharacters: number): string {
24
+ if (text.length <= maxCharacters) return text;
25
+ return `${text.slice(0, Math.max(0, maxCharacters - 1)).trimEnd()}…`;
26
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@alasano/pi-exa",
3
+ "version": "0.0.1",
4
+ "description": "Exa-powered web search, content retrieval, answers, and agentic search tools for pi",
5
+ "keywords": [
6
+ "pi-package"
7
+ ],
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/alasano/house-of-pi",
12
+ "directory": "packages/pi-exa"
13
+ },
14
+ "type": "module",
15
+ "engines": {
16
+ "node": ">=22.19.0"
17
+ },
18
+ "scripts": {
19
+ "typecheck": "tsc --noEmit",
20
+ "test": "vitest run"
21
+ },
22
+ "pi": {
23
+ "extensions": [
24
+ "./extensions"
25
+ ],
26
+ "skills": [
27
+ "./skills"
28
+ ],
29
+ "image": "https://raw.githubusercontent.com/alasano/house-of-pi/master/packages/pi-exa/assets/screenshot.png"
30
+ },
31
+ "files": [
32
+ "extensions",
33
+ "skills",
34
+ "assets",
35
+ "README.md"
36
+ ],
37
+ "peerDependencies": {
38
+ "@earendil-works/pi-coding-agent": "*",
39
+ "@earendil-works/pi-tui": "*",
40
+ "@sinclair/typebox": "*"
41
+ },
42
+ "devDependencies": {
43
+ "@earendil-works/pi-coding-agent": "^0.79.9",
44
+ "@earendil-works/pi-tui": "^0.79.9"
45
+ }
46
+ }
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: exa-search
3
+ description: Web research with Exa search, advanced search, URL fetch, and sourced answers. Use when current web information, source discovery, page extraction, or sourced web answers are needed.
4
+ ---
5
+
6
+ # Exa Search
7
+
8
+ Use Exa with a search-first, context-disciplined workflow. Prefer small, high-signal calls that discover sources, then fetch only the pages that are worth reading.
9
+
10
+ ## Tool Choice
11
+
12
+ - Use `web_search_exa` for normal web searches, current information lookup, source discovery, and quick orientation. It returns compact highlights.
13
+ - Use `web_search_advanced_exa` when filters or content controls matter: domains, dates, categories, freshness, summaries, highlights, subpages, response-level context, or extracted text. It returns full per-result text by default; set `textMaxCharacters` when that would be too much.
14
+ - Use `web_fetch_exa` after search when a specific URL needs fuller markdown content. Fetch selected URLs only.
15
+ - Use `web_answer_exa` when a direct answer with citations is more useful than a result list, especially for factual questions or concise sourced summaries.
16
+ - Use `web_agent_exa` only for deeper Agent workflows: multi-hop research, list building, row enrichment, entity research across many fields, or structured outputs that require broader web work than one search/answer/fetch call. Before using `web_agent_exa` or any `web_agent_*_exa` lifecycle tool, read `references/web-agent-exa.md`.
17
+ - If the user's task could benefit from Exa Agent but would cost more or run longer than normal search, briefly recommend Agent as an option instead of using it silently.
18
+ - After starting `web_agent_exa` with `mode: "background"`, do not repeatedly poll the run. The extension tracks it and sends a compact follow-up when it completes. Treat that follow-up as a notification, not the full result; call `web_agent_get_exa` before answering with detailed findings from the run.
19
+
20
+ ## Search Workflow
21
+
22
+ 1. Start with `web_search_exa` unless you already know you need filters or page text.
23
+ 2. Read result titles, URLs, and highlights. Identify the best sources before doing another call.
24
+ 3. Use `web_search_advanced_exa` to narrow by domain, date, category, geography, freshness, or content extraction. Remember that advanced search returns text by default.
25
+ 4. Use `web_fetch_exa` only for URLs that need closer inspection.
26
+ 5. Deduplicate similar results before answering. Keep only the best representative source for repeated articles, mirrors, copied docs, or forks.
27
+
28
+ ## Query Patterns
29
+
30
+ - General research: describe the ideal source, not just keywords. Example: `independent analysis of 2026 US EV tax credit changes`.
31
+ - Current events: include names, dates, organizations, and the event type. Use advanced search with date filters when recency matters.
32
+ - Official documentation: include the product, API, version, and the exact concept. Prefer `includeDomains` for official domains when the source matters.
33
+ - Code examples: include language, framework, major version, package name, and exact API. Example: `TypeScript React 19 useActionState form example`.
34
+ - Error/debugging searches: include the exact error string, runtime, library, and version when known.
35
+ - Comparison research: search each side explicitly, then dedupe and compare sources instead of relying on one broad query.
36
+
37
+ ## Advanced Search Recipes
38
+
39
+ - Domain-restricted research: set `includeDomains` for official docs, vendor docs, standards bodies, or trusted publications.
40
+ - Exclusions: set `excludeDomains` for sources that are duplicated, low quality, or not relevant to the user.
41
+ - Date-sensitive research: use published or crawl date filters when older sources would mislead the answer.
42
+ - Category search: use categories such as news, company, people, research paper, GitHub, PDF, financial report, or personal site when the source type matters.
43
+ - Text extraction: advanced search requests full text by default. Set `textMaxCharacters` when each result should be bounded, especially for broad queries or many results.
44
+ - Response context: set `contextMaxCharacters` when you want Exa to return a compact combined context string in addition to per-result fields.
45
+ - Highlights: set `enableHighlights` when you want compact evidence in addition to text. Add `highlightsQuery` when the highlight should focus on a specific claim or API.
46
+ - Summaries: set `enableSummary` when a short page-level summary is more useful than reading result text. Add `summaryQuery` to focus the summary.
47
+ - Freshness: use `maxAgeHours` when cached content could be stale. Use small values for fast-moving pages and omit it when freshness is not important.
48
+ - Subpages: use `subpages` and `subpageTarget` for docs sites where the useful answer may live below a landing page.
49
+
50
+ ## Fetch Guidance
51
+
52
+ - Fetch after choosing URLs from search results, not as a substitute for search.
53
+ - Keep `maxCharacters` tight by default. Increase it only when the selected URL is clearly worth reading in detail.
54
+ - For several candidate URLs, fetch the strongest one or two first, then decide whether more context is needed.
55
+ - Treat fetched text as more complete than search highlights, and keep track of where it came from.
56
+
57
+ ## Answer Guidance
58
+
59
+ - Use `web_answer_exa` for direct factual questions, quick sourced summaries, and cases where a sourced answer is enough.
60
+ - Use search instead when you need to compare sources, inspect exact wording, evaluate freshness, or gather multiple perspectives.
61
+ - Set `text: true` only when citation page text is needed; it can make the result larger.
62
+ - Use `outputSchema` when the answer must be structured JSON.
63
+
64
+ ## Agent Guidance
65
+
66
+ `web_agent_exa` is a higher-compute async workflow tool, not the default search path. Use it when the job genuinely needs Agent behavior, and read `references/web-agent-exa.md` first for effort choices, foreground/background mode, schema design, lifecycle tools, cost notes, background tracking, and examples.
67
+
68
+ ## Context Discipline
69
+
70
+ - Prefer highlights, summaries, and short fetched excerpts before large text dumps.
71
+ - Use one good query before issuing variants. Refine based on observed sources.
72
+ - Keep enough source information to support web-dependent claims when the answer needs it.
73
+ - Separate what Exa returned from your own synthesis.
74
+ - Do not paste raw Exa output when a concise answer with citations is enough.
75
+ - If the result set is noisy, narrow the query or filters before fetching.
76
+
77
+ ## Output Guidance
78
+
79
+ Answer in the format the user asked for. If no format was specified, give a clear, concise response that uses the searched information without dumping raw tool output.
80
+
81
+ Include source links, excerpts, or uncertainty notes when they are useful for the task, when the user asked for them, or when the answer depends on a specific current source. Keep fetched excerpts short unless the user asks for detail.