@jarcelao/pi-exa-api 0.2.0 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-exa-api
2
2
 
3
- Web search and content fetching for [pi](https://pi.dev) via the [Exa API](https://exa.ai/).
3
+ Web search, content fetching, and code context for [pi](https://pi.dev) via the [Exa API](https://exa.ai/).
4
4
 
5
5
  ## Installation
6
6
 
@@ -66,6 +66,23 @@ Fetch the content from https://example.com/article
66
66
  - `summary` - AI-generated summary
67
67
  - `maxCharacters` (optional) - Maximum characters to return (1000-100000)
68
68
 
69
+ ### Code Context
70
+
71
+ The agent can use `exa_code_context` to find code snippets and examples from open source libraries and repositories:
72
+
73
+ ```
74
+ Find examples of React hooks for state management
75
+ ```
76
+
77
+ It's ideal for understanding how libraries, frameworks, or programming concepts are implemented in practice.
78
+
79
+ **Parameters:**
80
+
81
+ - `query` (required) - Search query for code snippets and examples (1-2000 characters)
82
+ - `tokensNum` (optional) - Token limit for the response:
83
+ - `"dynamic"` (default) - Automatically determine optimal response length
84
+ - `50-100000` - Specific number of tokens (5000 is a good default, use 10000 when more context is needed)
85
+
69
86
  ## Development
70
87
 
71
88
  ```bash
@@ -298,6 +298,7 @@ describe("Fetch Result Formatting", () => {
298
298
 
299
299
  it("should format highlights", () => {
300
300
  const result = {
301
+ title: "Test Page",
301
302
  url: "https://example.com",
302
303
  highlights: ["Key point 1", "Key point 2"],
303
304
  };
@@ -311,6 +312,7 @@ describe("Fetch Result Formatting", () => {
311
312
 
312
313
  it("should format summary", () => {
313
314
  const result = {
315
+ title: "Test Page",
314
316
  url: "https://example.com",
315
317
  summary: "This page is about...",
316
318
  };
@@ -324,9 +326,9 @@ describe("Fetch Result Formatting", () => {
324
326
  const result = {
325
327
  url: "https://example.com",
326
328
  text: "Content only",
327
- };
329
+ } as { title?: string; url: string; text: string };
328
330
 
329
- const formatted = formatFetchResult(result, "text");
331
+ const formatted = formatFetchResult(result as Parameters<typeof formatFetchResult>[0], "text");
330
332
  expect(formatted).toContain("https://example.com");
331
333
  expect(formatted).not.toContain("Title:");
332
334
  });
@@ -485,6 +487,60 @@ describe("Code Context Parameter Validation", () => {
485
487
  });
486
488
  });
487
489
 
490
+ describe("tokensNum Type Coercion", () => {
491
+ // Helper function that mirrors the type coercion logic in exa_code_context execute
492
+ function coerceTokensNum(tokensNum: string | number | undefined): string | number {
493
+ const value = tokensNum ?? "dynamic";
494
+ if (typeof value === "string" && value !== "dynamic") {
495
+ return Number(value);
496
+ }
497
+ return value;
498
+ }
499
+
500
+ it("should return 'dynamic' when tokensNum is undefined", () => {
501
+ const result = coerceTokensNum(undefined);
502
+ expect(result).toBe("dynamic");
503
+ expect(typeof result).toBe("string");
504
+ });
505
+
506
+ it("should preserve 'dynamic' string", () => {
507
+ const result = coerceTokensNum("dynamic");
508
+ expect(result).toBe("dynamic");
509
+ expect(typeof result).toBe("string");
510
+ });
511
+
512
+ it("should preserve numeric tokens", () => {
513
+ const result = coerceTokensNum(5000);
514
+ expect(result).toBe(5000);
515
+ expect(typeof result).toBe("number");
516
+ });
517
+
518
+ it("should coerce numeric string to number", () => {
519
+ const result = coerceTokensNum("5000");
520
+ expect(result).toBe(5000);
521
+ expect(typeof result).toBe("number");
522
+ });
523
+
524
+ it("should coerce various numeric strings", () => {
525
+ expect(coerceTokensNum("1000")).toBe(1000);
526
+ expect(coerceTokensNum("10000")).toBe(10000);
527
+ expect(coerceTokensNum("3000")).toBe(3000);
528
+ expect(typeof coerceTokensNum("5000")).toBe("number");
529
+ });
530
+
531
+ it("should handle boundary token values", () => {
532
+ expect(coerceTokensNum(50)).toBe(50);
533
+ expect(coerceTokensNum(100000)).toBe(100000);
534
+ });
535
+
536
+ it("should not modify actual numbers", () => {
537
+ const originalNumber = 7500;
538
+ const result = coerceTokensNum(originalNumber);
539
+ expect(result).toBe(originalNumber);
540
+ expect(result).toBe(7500);
541
+ });
542
+ });
543
+
488
544
  describe("Extension Registration", () => {
489
545
  function createMockExtensionAPI() {
490
546
  const tools: unknown[] = [];
@@ -622,7 +678,6 @@ describe("Extension Registration", () => {
622
678
  mockResult,
623
679
  { expanded: false, isPartial: false },
624
680
  mockTheme,
625
- {},
626
681
  );
627
682
 
628
683
  expect(rendered.text).toContain("Test Page");
@@ -654,7 +709,6 @@ describe("Extension Registration", () => {
654
709
  mockResult,
655
710
  { expanded: false, isPartial: false },
656
711
  mockTheme,
657
- {},
658
712
  );
659
713
 
660
714
  expect(rendered.text).toContain("Fetched");
@@ -688,7 +742,6 @@ describe("Extension Registration", () => {
688
742
  mockResult,
689
743
  { expanded: false, isPartial: false },
690
744
  mockTheme,
691
- {},
692
745
  );
693
746
 
694
747
  expect(rendered.text).toContain("502 sources");
@@ -722,7 +775,6 @@ describe("Extension Registration", () => {
722
775
  mockResult,
723
776
  { expanded: false, isPartial: false },
724
777
  mockTheme,
725
- {},
726
778
  );
727
779
 
728
780
  expect(rendered.text).toContain("100 sources");
@@ -751,7 +803,6 @@ describe("Extension Registration", () => {
751
803
  mockResult,
752
804
  { expanded: false, isPartial: false },
753
805
  mockTheme,
754
- {},
755
806
  );
756
807
 
757
808
  expect(rendered.text).toContain("Some code context");
@@ -9,17 +9,18 @@
9
9
  * Also registers the /exa-status command to check API key configuration.
10
10
  */
11
11
 
12
+ import { mkdtemp, writeFile } from "node:fs/promises";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
12
15
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
13
- import { Text } from "@mariozechner/pi-tui";
14
- import { Type, type Static } from "@sinclair/typebox";
15
- import { StringEnum } from "@mariozechner/pi-ai";
16
16
  import {
17
- truncateHead,
18
17
  DEFAULT_MAX_BYTES,
19
18
  DEFAULT_MAX_LINES,
20
19
  formatSize,
21
- writeTempFile,
20
+ truncateHead,
22
21
  } from "@mariozechner/pi-coding-agent";
22
+ import { Text } from "@mariozechner/pi-tui";
23
+ import { Type, type Static } from "@mariozechner/pi-ai";
23
24
  import Exa from "exa-js";
24
25
 
25
26
  // API Key Management
@@ -29,6 +30,15 @@ function getApiKey(): string | undefined {
29
30
  return key && key.length > 0 ? key : undefined;
30
31
  }
31
32
 
33
+ // Temp File Helper
34
+
35
+ async function writeTempFile(content: string): Promise<string> {
36
+ const tempDir = await mkdtemp(join(tmpdir(), "pi-exa-"));
37
+ const tempFile = join(tempDir, "output.txt");
38
+ await writeFile(tempFile, content, "utf8");
39
+ return tempFile;
40
+ }
41
+
32
42
  // Type Definitions
33
43
 
34
44
  type SearchContentType = "text" | "highlights" | "summary" | "none";
@@ -251,7 +261,14 @@ export default function exaSearchExtension(pi: ExtensionAPI): void {
251
261
  query: Type.String({
252
262
  description: "Natural language search query",
253
263
  }),
254
- contentType: Type.Optional(StringEnum(["text", "highlights", "summary", "none"] as const)),
264
+ contentType: Type.Optional(
265
+ Type.Union([
266
+ Type.Literal("text"),
267
+ Type.Literal("highlights"),
268
+ Type.Literal("summary"),
269
+ Type.Literal("none"),
270
+ ]),
271
+ ),
255
272
  numResults: Type.Optional(
256
273
  Type.Number({
257
274
  description: "Number of results (1-100)",
@@ -312,7 +329,7 @@ export default function exaSearchExtension(pi: ExtensionAPI): void {
312
329
  let result = truncation.content;
313
330
 
314
331
  if (truncation.truncated) {
315
- const tempFile = writeTempFile(output);
332
+ const tempFile = await writeTempFile(output);
316
333
  result += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
317
334
  result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
318
335
  result += ` Full output saved to: ${tempFile}]`;
@@ -338,7 +355,7 @@ export default function exaSearchExtension(pi: ExtensionAPI): void {
338
355
  return new Text(text, 0, 0);
339
356
  },
340
357
 
341
- renderResult(result, { expanded: _expanded, isPartial: _isPartial }, theme, _context) {
358
+ renderResult(result, _options, theme) {
342
359
  const details = result.details as SearchDetails | undefined;
343
360
 
344
361
  if (!details) {
@@ -357,7 +374,9 @@ export default function exaSearchExtension(pi: ExtensionAPI): void {
357
374
  url: Type.String({
358
375
  description: "URL to fetch content from",
359
376
  }),
360
- contentType: Type.Optional(StringEnum(["text", "highlights", "summary"] as const)),
377
+ contentType: Type.Optional(
378
+ Type.Union([Type.Literal("text"), Type.Literal("highlights"), Type.Literal("summary")]),
379
+ ),
361
380
  maxCharacters: Type.Optional(
362
381
  Type.Number({
363
382
  description: "Maximum characters to return",
@@ -427,7 +446,7 @@ export default function exaSearchExtension(pi: ExtensionAPI): void {
427
446
  let content = truncation.content;
428
447
 
429
448
  if (truncation.truncated) {
430
- const tempFile = writeTempFile(output);
449
+ const tempFile = await writeTempFile(output);
431
450
  content += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
432
451
  content += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
433
452
  content += ` Full output saved to: ${tempFile}]`;
@@ -453,7 +472,7 @@ export default function exaSearchExtension(pi: ExtensionAPI): void {
453
472
  return new Text(text, 0, 0);
454
473
  },
455
474
 
456
- renderResult(result, { expanded: _expanded, isPartial: _isPartial }, theme, _context) {
475
+ renderResult(result, _options, theme) {
457
476
  const details = result.details as FetchDetails | undefined;
458
477
 
459
478
  if (!details) {
@@ -508,7 +527,14 @@ export default function exaSearchExtension(pi: ExtensionAPI): void {
508
527
  throw createMissingApiKeyError();
509
528
  }
510
529
 
511
- const tokensNum = params.tokensNum ?? "dynamic";
530
+ // Ensure tokensNum is the correct type: number or "dynamic"
531
+ // The schema accepts both string and number, but the Exa API requires:
532
+ // - A number (e.g., 5000)
533
+ // - The literal string "dynamic"
534
+ let tokensNum: string | number = params.tokensNum ?? "dynamic";
535
+ if (typeof tokensNum === "string" && tokensNum !== "dynamic") {
536
+ tokensNum = Number(tokensNum);
537
+ }
512
538
 
513
539
  let response;
514
540
  try {
@@ -545,7 +571,7 @@ export default function exaSearchExtension(pi: ExtensionAPI): void {
545
571
  let result = truncation.content;
546
572
 
547
573
  if (truncation.truncated) {
548
- const tempFile = writeTempFile(output);
574
+ const tempFile = await writeTempFile(output);
549
575
  result += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
550
576
  result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
551
577
  result += ` Full output saved to: ${tempFile}]`;
@@ -574,7 +600,7 @@ export default function exaSearchExtension(pi: ExtensionAPI): void {
574
600
  return new Text(text, 0, 0);
575
601
  },
576
602
 
577
- renderResult(result, { expanded: _expanded, isPartial: _isPartial }, theme, _context) {
603
+ renderResult(result, _options, theme) {
578
604
  const details = result.details as CodeContextDetails | undefined;
579
605
 
580
606
  if (!details) {
@@ -584,7 +610,10 @@ export default function exaSearchExtension(pi: ExtensionAPI): void {
584
610
 
585
611
  const cost = details.cost ? ` • $${details.cost.total.toFixed(6)}` : "";
586
612
  return new Text(
587
- theme.fg("success", `✓ ${details.resultsCount} sources • ${details.outputTokens} tokens${cost}`),
613
+ theme.fg(
614
+ "success",
615
+ `✓ ${details.resultsCount} sources • ${details.outputTokens} tokens${cost}`,
616
+ ),
588
617
  0,
589
618
  0,
590
619
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jarcelao/pi-exa-api",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Web search and content fetching for pi via the Exa API",
5
5
  "keywords": [
6
6
  "pi-package"
package/tsconfig.json CHANGED
@@ -8,8 +8,9 @@
8
8
  "skipLibCheck": true,
9
9
  "noEmit": true,
10
10
  "resolveJsonModule": true,
11
+ "allowImportingTsExtensions": true,
11
12
  "types": ["node"]
12
13
  },
13
- "include": ["*.ts"],
14
+ "include": ["*.ts", "extensions/**/*.ts"],
14
15
  "exclude": ["node_modules"]
15
16
  }