@jarcelao/pi-exa-api 0.3.0 → 0.3.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.
- package/README.md +1 -1
- package/extensions/formatters.ts +2 -2
- package/extensions/index.ts +1 -21
- package/extensions/tools/code-context.ts +24 -65
- package/extensions/tools/fetch.ts +20 -55
- package/extensions/tools/search.ts +17 -53
- package/extensions/tools/shared.ts +106 -0
- package/package.json +6 -6
- package/tests/api-key.test.ts +2 -1
- package/tests/content-type-mapping.test.ts +1 -1
- package/tests/extension-registration.test.ts +1 -1
- package/tests/formatting.test.ts +1 -1
- package/extensions/temp-file.ts +0 -19
package/README.md
CHANGED
package/extensions/formatters.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Result formatting utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { keyHint } from "@
|
|
6
|
-
import type { Theme } from "@
|
|
5
|
+
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
|
|
8
8
|
import type {
|
|
9
9
|
ExaSearchResult,
|
package/extensions/index.ts
CHANGED
|
@@ -9,34 +9,14 @@
|
|
|
9
9
|
* Also registers the /exa-status command to check API key configuration.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
12
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
|
|
14
14
|
import { getApiKey } from "./api-key.ts";
|
|
15
|
-
import { createMissingApiKeyError } from "./errors.ts";
|
|
16
|
-
import { mapSearchContentType, mapFetchContentType } from "./content-types.ts";
|
|
17
|
-
import {
|
|
18
|
-
formatSearchResults,
|
|
19
|
-
formatFetchResult,
|
|
20
|
-
formatCodeContextResult,
|
|
21
|
-
parseCostDollars,
|
|
22
|
-
} from "./formatters.ts";
|
|
23
15
|
|
|
24
16
|
import { createExaSearchTool } from "./tools/search.ts";
|
|
25
17
|
import { createExaFetchTool } from "./tools/fetch.ts";
|
|
26
18
|
import { createExaCodeContextTool } from "./tools/code-context.ts";
|
|
27
19
|
|
|
28
|
-
// Re-export all utilities for testing
|
|
29
|
-
export {
|
|
30
|
-
getApiKey,
|
|
31
|
-
createMissingApiKeyError,
|
|
32
|
-
mapSearchContentType,
|
|
33
|
-
mapFetchContentType,
|
|
34
|
-
formatSearchResults,
|
|
35
|
-
formatFetchResult,
|
|
36
|
-
formatCodeContextResult,
|
|
37
|
-
parseCostDollars,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
20
|
export default function exaSearchExtension(pi: ExtensionAPI): void {
|
|
41
21
|
pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
|
|
42
22
|
const hasKey = !!getApiKey();
|
|
@@ -2,28 +2,20 @@
|
|
|
2
2
|
* Exa Code Context tool definition
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import {
|
|
10
|
-
DEFAULT_MAX_BYTES,
|
|
11
|
-
DEFAULT_MAX_LINES,
|
|
12
|
-
defineTool,
|
|
13
|
-
formatSize,
|
|
14
|
-
truncateHead,
|
|
15
|
-
} from "@mariozechner/pi-coding-agent";
|
|
16
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
5
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
17
7
|
import { Type, type Static } from "typebox";
|
|
18
8
|
|
|
19
|
-
import {
|
|
20
|
-
import { createMissingApiKeyError } from "../errors.ts";
|
|
21
|
-
import {
|
|
22
|
-
formatCodeContextResult,
|
|
23
|
-
formatToolOutputPreview,
|
|
24
|
-
parseCostDollars,
|
|
25
|
-
} from "../formatters.ts";
|
|
9
|
+
import { formatCodeContextResult, parseCostDollars } from "../formatters.ts";
|
|
26
10
|
import type { CodeContextDetails, CodeContextResponse } from "../types.ts";
|
|
11
|
+
import {
|
|
12
|
+
requireApiKey,
|
|
13
|
+
truncateAndSave,
|
|
14
|
+
renderToolCall,
|
|
15
|
+
renderToolResult,
|
|
16
|
+
formatCost,
|
|
17
|
+
EXA_CONTEXT_API_URL,
|
|
18
|
+
} from "./shared.ts";
|
|
27
19
|
|
|
28
20
|
// Tool parameter schema
|
|
29
21
|
export const ExaCodeContextParams = Type.Object({
|
|
@@ -62,15 +54,9 @@ export function createExaCodeContextTool() {
|
|
|
62
54
|
_onUpdate: unknown,
|
|
63
55
|
_ctx: unknown,
|
|
64
56
|
) {
|
|
65
|
-
const apiKey =
|
|
66
|
-
if (!apiKey) {
|
|
67
|
-
throw createMissingApiKeyError();
|
|
68
|
-
}
|
|
57
|
+
const apiKey = requireApiKey();
|
|
69
58
|
|
|
70
59
|
// Ensure tokensNum is the correct type: number or "dynamic"
|
|
71
|
-
// The schema accepts both string and number, but the Exa API requires:
|
|
72
|
-
// - A number (e.g., 5000)
|
|
73
|
-
// - The literal string "dynamic"
|
|
74
60
|
let tokensNum: string | number = params.tokensNum ?? "dynamic";
|
|
75
61
|
if (typeof tokensNum === "string" && tokensNum !== "dynamic") {
|
|
76
62
|
tokensNum = Number(tokensNum);
|
|
@@ -78,7 +64,7 @@ export function createExaCodeContextTool() {
|
|
|
78
64
|
|
|
79
65
|
let response: CodeContextResponse;
|
|
80
66
|
try {
|
|
81
|
-
const httpResponse = await fetch(
|
|
67
|
+
const httpResponse = await fetch(EXA_CONTEXT_API_URL, {
|
|
82
68
|
method: "POST",
|
|
83
69
|
headers: {
|
|
84
70
|
"Content-Type": "application/json",
|
|
@@ -101,24 +87,8 @@ export function createExaCodeContextTool() {
|
|
|
101
87
|
throw new Error(`Exa Context API error: ${message}`);
|
|
102
88
|
}
|
|
103
89
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const truncation = truncateHead(output, {
|
|
107
|
-
maxLines: DEFAULT_MAX_LINES,
|
|
108
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
let result = truncation.content;
|
|
112
|
-
|
|
113
|
-
if (truncation.truncated) {
|
|
114
|
-
const tempDir = await mkdtemp(join(tmpdir(), "pi-exa-"));
|
|
115
|
-
const tempFile = join(tempDir, "output.txt");
|
|
116
|
-
await writeFile(tempFile, output, "utf8");
|
|
117
|
-
result += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
|
|
118
|
-
result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
|
|
119
|
-
result += ` Full output saved to: ${tempFile}]`;
|
|
120
|
-
}
|
|
121
|
-
|
|
90
|
+
const output = formatCodeContextResult(response);
|
|
91
|
+
const result = await truncateAndSave(output);
|
|
122
92
|
const cost = parseCostDollars(response.costDollars);
|
|
123
93
|
|
|
124
94
|
return {
|
|
@@ -133,31 +103,20 @@ export function createExaCodeContextTool() {
|
|
|
133
103
|
},
|
|
134
104
|
|
|
135
105
|
renderCall(args: CodeContextParams, theme: Theme) {
|
|
136
|
-
const preview = args.query.length > 50 ? args.query.slice(0, 50) + "..." : args.query;
|
|
137
106
|
const desc = `${args.tokensNum ?? "dynamic"} tokens`;
|
|
138
|
-
|
|
139
|
-
theme.fg("toolTitle", theme.bold("exa_code_context ")) +
|
|
140
|
-
theme.fg("muted", preview) +
|
|
141
|
-
theme.fg("dim", ` ${desc}`);
|
|
142
|
-
return new Text(text, 0, 0);
|
|
107
|
+
return renderToolCall("exa_code_context", args.query, desc, theme);
|
|
143
108
|
},
|
|
144
109
|
|
|
145
110
|
renderResult(result, options, theme, context) {
|
|
146
111
|
const details = result.details as CodeContextDetails | undefined;
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const preview = formatToolOutputPreview(result, options, theme);
|
|
159
|
-
text.setText(preview ? `${header}\n${preview}` : header);
|
|
160
|
-
return text;
|
|
112
|
+
const cost = details ? formatCost(details.cost) : "";
|
|
113
|
+
const header = details
|
|
114
|
+
? theme.fg(
|
|
115
|
+
"success",
|
|
116
|
+
`✓ ${details.resultsCount} sources • ${details.outputTokens} tokens${cost}`,
|
|
117
|
+
)
|
|
118
|
+
: "";
|
|
119
|
+
return renderToolResult(header, result, options, theme, context);
|
|
161
120
|
},
|
|
162
121
|
});
|
|
163
122
|
}
|
|
@@ -2,26 +2,21 @@
|
|
|
2
2
|
* Exa Fetch tool definition
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import type { ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import {
|
|
10
|
-
DEFAULT_MAX_BYTES,
|
|
11
|
-
DEFAULT_MAX_LINES,
|
|
12
|
-
defineTool,
|
|
13
|
-
formatSize,
|
|
14
|
-
truncateHead,
|
|
15
|
-
} from "@mariozechner/pi-coding-agent";
|
|
16
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
5
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
17
7
|
import { Type, type Static } from "typebox";
|
|
18
8
|
import Exa from "exa-js";
|
|
19
9
|
|
|
20
|
-
import { getApiKey } from "../api-key.ts";
|
|
21
|
-
import { createMissingApiKeyError } from "../errors.ts";
|
|
22
10
|
import { mapFetchContentType } from "../content-types.ts";
|
|
23
|
-
import { formatFetchResult
|
|
11
|
+
import { formatFetchResult } from "../formatters.ts";
|
|
24
12
|
import type { FetchContentType, FetchDetails, ExaSearchResult } from "../types.ts";
|
|
13
|
+
import {
|
|
14
|
+
requireApiKey,
|
|
15
|
+
truncateAndSave,
|
|
16
|
+
renderToolCall,
|
|
17
|
+
renderToolResult,
|
|
18
|
+
formatCost,
|
|
19
|
+
} from "./shared.ts";
|
|
25
20
|
|
|
26
21
|
// Tool parameter schema
|
|
27
22
|
export const ExaFetchParams = Type.Object({
|
|
@@ -58,11 +53,7 @@ export function createExaFetchTool() {
|
|
|
58
53
|
_onUpdate: unknown,
|
|
59
54
|
_ctx: ExtensionContext,
|
|
60
55
|
) {
|
|
61
|
-
const apiKey =
|
|
62
|
-
if (!apiKey) {
|
|
63
|
-
throw createMissingApiKeyError();
|
|
64
|
-
}
|
|
65
|
-
|
|
56
|
+
const apiKey = requireApiKey();
|
|
66
57
|
const exa = new Exa(apiKey);
|
|
67
58
|
|
|
68
59
|
const contentsOptions: {
|
|
@@ -96,23 +87,8 @@ export function createExaFetchTool() {
|
|
|
96
87
|
}
|
|
97
88
|
|
|
98
89
|
const result = response.results[0] as ExaSearchResult;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const truncation = truncateHead(output, {
|
|
102
|
-
maxLines: DEFAULT_MAX_LINES,
|
|
103
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
let content = truncation.content;
|
|
107
|
-
|
|
108
|
-
if (truncation.truncated) {
|
|
109
|
-
const tempDir = await mkdtemp(join(tmpdir(), "pi-exa-"));
|
|
110
|
-
const tempFile = join(tempDir, "output.txt");
|
|
111
|
-
await writeFile(tempFile, output, "utf8");
|
|
112
|
-
content += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
|
|
113
|
-
content += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
|
|
114
|
-
content += ` Full output saved to: ${tempFile}]`;
|
|
115
|
-
}
|
|
90
|
+
const output = formatFetchResult(result, (params.contentType ?? "text") as FetchContentType);
|
|
91
|
+
const content = await truncateAndSave(output);
|
|
116
92
|
|
|
117
93
|
return {
|
|
118
94
|
content: [{ type: "text", text: content }],
|
|
@@ -125,30 +101,19 @@ export function createExaFetchTool() {
|
|
|
125
101
|
},
|
|
126
102
|
|
|
127
103
|
renderCall(args: FetchParams, theme: Theme) {
|
|
128
|
-
const urlPreview = args.url.length > 40 ? args.url.slice(0, 40) + "..." : args.url;
|
|
129
104
|
const desc = args.contentType ?? "text";
|
|
130
|
-
|
|
131
|
-
theme.fg("toolTitle", theme.bold("exa_fetch ")) +
|
|
132
|
-
theme.fg("muted", urlPreview) +
|
|
133
|
-
theme.fg("dim", ` ${desc}`);
|
|
134
|
-
return new Text(text, 0, 0);
|
|
105
|
+
return renderToolCall("exa_fetch", args.url, desc, theme);
|
|
135
106
|
},
|
|
136
107
|
|
|
137
108
|
renderResult(result, options, theme, context) {
|
|
138
109
|
const details = result.details as FetchDetails | undefined;
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (details) {
|
|
143
|
-
const cost = details.cost ? ` • $${details.cost.total.toFixed(6)}` : "";
|
|
144
|
-
header = details.title
|
|
110
|
+
const cost = details ? formatCost(details.cost) : "";
|
|
111
|
+
const header = details
|
|
112
|
+
? details.title
|
|
145
113
|
? theme.fg("success", `✓ ${details.title}${cost}`)
|
|
146
|
-
: theme.fg("success", `✓ Fetched${cost}`)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const preview = formatToolOutputPreview(result, options, theme);
|
|
150
|
-
text.setText(preview ? `${header}\n${preview}` : header);
|
|
151
|
-
return text;
|
|
114
|
+
: theme.fg("success", `✓ Fetched${cost}`)
|
|
115
|
+
: "";
|
|
116
|
+
return renderToolResult(header, result, options, theme, context);
|
|
152
117
|
},
|
|
153
118
|
});
|
|
154
119
|
}
|
|
@@ -2,26 +2,21 @@
|
|
|
2
2
|
* Exa Search tool definition
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import type { ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import {
|
|
10
|
-
DEFAULT_MAX_BYTES,
|
|
11
|
-
DEFAULT_MAX_LINES,
|
|
12
|
-
defineTool,
|
|
13
|
-
formatSize,
|
|
14
|
-
truncateHead,
|
|
15
|
-
} from "@mariozechner/pi-coding-agent";
|
|
16
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
5
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
17
7
|
import { Type, type Static } from "typebox";
|
|
18
8
|
import Exa from "exa-js";
|
|
19
9
|
|
|
20
|
-
import { getApiKey } from "../api-key.ts";
|
|
21
|
-
import { createMissingApiKeyError } from "../errors.ts";
|
|
22
10
|
import { mapSearchContentType } from "../content-types.ts";
|
|
23
|
-
import { formatSearchResults
|
|
11
|
+
import { formatSearchResults } from "../formatters.ts";
|
|
24
12
|
import type { SearchContentType, SearchDetails, ExaSearchResult } from "../types.ts";
|
|
13
|
+
import {
|
|
14
|
+
requireApiKey,
|
|
15
|
+
truncateAndSave,
|
|
16
|
+
renderToolCall,
|
|
17
|
+
renderToolResult,
|
|
18
|
+
formatCost,
|
|
19
|
+
} from "./shared.ts";
|
|
25
20
|
|
|
26
21
|
// Tool parameter schema
|
|
27
22
|
export const ExaSearchParams = Type.Object({
|
|
@@ -63,11 +58,7 @@ export function createExaSearchTool() {
|
|
|
63
58
|
_onUpdate: unknown,
|
|
64
59
|
_ctx: ExtensionContext,
|
|
65
60
|
) {
|
|
66
|
-
const apiKey =
|
|
67
|
-
if (!apiKey) {
|
|
68
|
-
throw createMissingApiKeyError();
|
|
69
|
-
}
|
|
70
|
-
|
|
61
|
+
const apiKey = requireApiKey();
|
|
71
62
|
const numResults = Math.max(1, Math.min(100, params.numResults ?? 10));
|
|
72
63
|
const exa = new Exa(apiKey);
|
|
73
64
|
|
|
@@ -89,26 +80,12 @@ export function createExaSearchTool() {
|
|
|
89
80
|
throw new Error(`Exa API error: ${message}`);
|
|
90
81
|
}
|
|
91
82
|
|
|
92
|
-
|
|
83
|
+
const output = formatSearchResults({
|
|
93
84
|
results: response.results as ExaSearchResult[],
|
|
94
85
|
costDollars: response.costDollars as { total: number } | undefined,
|
|
95
86
|
});
|
|
96
87
|
|
|
97
|
-
const
|
|
98
|
-
maxLines: DEFAULT_MAX_LINES,
|
|
99
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
let result = truncation.content;
|
|
103
|
-
|
|
104
|
-
if (truncation.truncated) {
|
|
105
|
-
const tempDir = await mkdtemp(join(tmpdir(), "pi-exa-"));
|
|
106
|
-
const tempFile = join(tempDir, "output.txt");
|
|
107
|
-
await writeFile(tempFile, output, "utf8");
|
|
108
|
-
result += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
|
|
109
|
-
result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
|
|
110
|
-
result += ` Full output saved to: ${tempFile}]`;
|
|
111
|
-
}
|
|
88
|
+
const result = await truncateAndSave(output);
|
|
112
89
|
|
|
113
90
|
return {
|
|
114
91
|
content: [{ type: "text", text: result }],
|
|
@@ -121,28 +98,15 @@ export function createExaSearchTool() {
|
|
|
121
98
|
},
|
|
122
99
|
|
|
123
100
|
renderCall(args: SearchParams, theme: Theme) {
|
|
124
|
-
const preview = args.query.length > 50 ? args.query.slice(0, 50) + "..." : args.query;
|
|
125
101
|
const desc = `${args.numResults ?? 10} results • ${args.contentType ?? "highlights"}`;
|
|
126
|
-
|
|
127
|
-
theme.fg("toolTitle", theme.bold("exa_search ")) +
|
|
128
|
-
theme.fg("muted", preview) +
|
|
129
|
-
theme.fg("dim", ` ${desc}`);
|
|
130
|
-
return new Text(text, 0, 0);
|
|
102
|
+
return renderToolCall("exa_search", args.query, desc, theme);
|
|
131
103
|
},
|
|
132
104
|
|
|
133
105
|
renderResult(result, options, theme, context) {
|
|
134
106
|
const details = result.details as SearchDetails | undefined;
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (details) {
|
|
139
|
-
const cost = details.cost ? ` • $${details.cost.total.toFixed(6)}` : "";
|
|
140
|
-
header = theme.fg("success", `✓ ${details.numResults} results${cost}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const preview = formatToolOutputPreview(result, options, theme);
|
|
144
|
-
text.setText(preview ? `${header}\n${preview}` : header);
|
|
145
|
-
return text;
|
|
107
|
+
const cost = details ? formatCost(details.cost) : "";
|
|
108
|
+
const header = details ? theme.fg("success", `✓ ${details.numResults} results${cost}`) : "";
|
|
109
|
+
return renderToolResult(header, result, options, theme, context);
|
|
146
110
|
},
|
|
147
111
|
});
|
|
148
112
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for Exa tools
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_MAX_BYTES,
|
|
10
|
+
DEFAULT_MAX_LINES,
|
|
11
|
+
formatSize,
|
|
12
|
+
truncateHead,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
15
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
|
|
17
|
+
import { getApiKey } from "../api-key.ts";
|
|
18
|
+
import { createMissingApiKeyError } from "../errors.ts";
|
|
19
|
+
import { formatToolOutputPreview } from "../formatters.ts";
|
|
20
|
+
|
|
21
|
+
/** Exa Context API base URL */
|
|
22
|
+
export const EXA_CONTEXT_API_URL = "https://api.exa.ai/context";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Require an API key to be configured, throwing if missing.
|
|
26
|
+
* @returns The API key
|
|
27
|
+
*/
|
|
28
|
+
export function requireApiKey(): string {
|
|
29
|
+
const apiKey = getApiKey();
|
|
30
|
+
if (!apiKey) {
|
|
31
|
+
throw createMissingApiKeyError();
|
|
32
|
+
}
|
|
33
|
+
return apiKey;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Truncate output and save to temp file if needed.
|
|
38
|
+
* @param output - The full output string
|
|
39
|
+
* @returns Object with the (possibly truncated) content string
|
|
40
|
+
*/
|
|
41
|
+
export async function truncateAndSave(output: string): Promise<string> {
|
|
42
|
+
const truncation = truncateHead(output, {
|
|
43
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
44
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
let content = truncation.content;
|
|
48
|
+
|
|
49
|
+
if (truncation.truncated) {
|
|
50
|
+
const tempDir = await mkdtemp(join(tmpdir(), "pi-exa-"));
|
|
51
|
+
const tempFile = join(tempDir, "output.txt");
|
|
52
|
+
await writeFile(tempFile, output, "utf8");
|
|
53
|
+
content += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
|
|
54
|
+
content += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
|
|
55
|
+
content += ` Full output saved to: ${tempFile}]`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return content;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Format the tool call preview for TUI display.
|
|
63
|
+
* @param label - The tool label (e.g., "exa_search")
|
|
64
|
+
* @param preview - The main preview text
|
|
65
|
+
* @param desc - The description text
|
|
66
|
+
* @param theme - The theme object
|
|
67
|
+
*/
|
|
68
|
+
export function renderToolCall(label: string, preview: string, desc: string, theme: Theme): Text {
|
|
69
|
+
const truncatedPreview = preview.length > 50 ? preview.slice(0, 50) + "..." : preview;
|
|
70
|
+
const text =
|
|
71
|
+
theme.fg("toolTitle", theme.bold(`${label} `)) +
|
|
72
|
+
theme.fg("muted", truncatedPreview) +
|
|
73
|
+
theme.fg("dim", ` ${desc}`);
|
|
74
|
+
return new Text(text, 0, 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Format the tool result for TUI display with optional header and preview.
|
|
79
|
+
* @param details - The details object with optional cost info
|
|
80
|
+
* @param headerText - The header text to display
|
|
81
|
+
* @param result - The full result object
|
|
82
|
+
* @param options - The display options
|
|
83
|
+
* @param theme - The theme object
|
|
84
|
+
* @param context - The context object
|
|
85
|
+
*/
|
|
86
|
+
export function renderToolResult(
|
|
87
|
+
headerText: string,
|
|
88
|
+
result: { content: Array<{ type: string; text?: string }> },
|
|
89
|
+
options: { expanded: boolean },
|
|
90
|
+
theme: Theme,
|
|
91
|
+
context: { lastComponent?: unknown },
|
|
92
|
+
): Text {
|
|
93
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
94
|
+
const preview = formatToolOutputPreview(result, options, theme);
|
|
95
|
+
text.setText(preview ? `${headerText}\n${preview}` : headerText);
|
|
96
|
+
return text;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Format cost for display.
|
|
101
|
+
* @param cost - Cost object with total property
|
|
102
|
+
* @returns Formatted cost string or empty string
|
|
103
|
+
*/
|
|
104
|
+
export function formatCost(cost?: { total: number }): string {
|
|
105
|
+
return cost ? ` • $${cost.total.toFixed(6)}` : "";
|
|
106
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarcelao/pi-exa-api",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Web search and content fetching for pi via the Exa API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -22,18 +22,18 @@
|
|
|
22
22
|
"format:check": "oxfmt --check"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"exa-js": "^2.
|
|
25
|
+
"exa-js": "^2.12.1"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^25.5.0",
|
|
29
|
-
"oxfmt": "^0.
|
|
29
|
+
"oxfmt": "^0.48.0",
|
|
30
30
|
"oxlint": "^1.56.0",
|
|
31
|
-
"typescript": "^
|
|
31
|
+
"typescript": "^6.0.3",
|
|
32
32
|
"vitest": "^4.1.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@
|
|
36
|
-
"@
|
|
35
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
36
|
+
"@earendil-works/pi-tui": "*",
|
|
37
37
|
"typebox": "*"
|
|
38
38
|
},
|
|
39
39
|
"pi": {
|
package/tests/api-key.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
-
import { getApiKey
|
|
2
|
+
import { getApiKey } from "../extensions/api-key.ts";
|
|
3
|
+
import { createMissingApiKeyError } from "../extensions/errors.ts";
|
|
3
4
|
|
|
4
5
|
describe("API Key Management", () => {
|
|
5
6
|
afterEach(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { mapSearchContentType, mapFetchContentType } from "../extensions/
|
|
2
|
+
import { mapSearchContentType, mapFetchContentType } from "../extensions/content-types.ts";
|
|
3
3
|
|
|
4
4
|
describe("ContentType Mapping (Search)", () => {
|
|
5
5
|
it('should map "text" to contents.text', () => {
|
|
@@ -23,7 +23,7 @@ function createMockExtensionAPI() {
|
|
|
23
23
|
|
|
24
24
|
function setup() {
|
|
25
25
|
const api = createMockExtensionAPI();
|
|
26
|
-
exaSearchExtension(api as unknown as import("@
|
|
26
|
+
exaSearchExtension(api as unknown as import("@earendil-works/pi-coding-agent").ExtensionAPI);
|
|
27
27
|
return api;
|
|
28
28
|
}
|
|
29
29
|
|
package/tests/formatting.test.ts
CHANGED
package/extensions/temp-file.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Temp file utilities
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Write content to a temporary file.
|
|
11
|
-
* @param content - The content to write
|
|
12
|
-
* @returns The path to the created temporary file
|
|
13
|
-
*/
|
|
14
|
-
export async function writeTempFile(content: string): Promise<string> {
|
|
15
|
-
const tempDir = await mkdtemp(join(tmpdir(), "pi-exa-"));
|
|
16
|
-
const tempFile = join(tempDir, "output.txt");
|
|
17
|
-
await writeFile(tempFile, content, "utf8");
|
|
18
|
-
return tempFile;
|
|
19
|
-
}
|