From exa-pack
Sets up Node.js/TypeScript project for Exa API with structure, tsx hot reload, Vitest unit/integration tests, and mock responses for fast iteration.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin exa-packThis skill is limited to using the following tools:
Set up a fast, reproducible local development workflow for Exa integrations. Covers project structure, mock responses for unit tests, integration test patterns, and hot-reload configuration.
Applies production-ready exa-js SDK patterns: client singletons, typed search wrappers, error handling for TypeScript Exa API integrations.
Sets up local dev environment for Perplexity Sonar API with TypeScript OpenAI client wrapper, Vitest testing, mocking fixtures, and hot reload for fast iteration.
Sets up Vitest testing by detecting project type (React, Node, Hono, Cloudflare Workers), generating config/tests/utilities, adding scripts, and migrating from Jest.
Share bugs, ideas, or general feedback.
Set up a fast, reproducible local development workflow for Exa integrations. Covers project structure, mock responses for unit tests, integration test patterns, and hot-reload configuration.
exa-js installed and EXA_API_KEY configuredvitest for testing (or jest)my-exa-project/
├── src/
│ ├── exa/
│ │ ├── client.ts # Singleton Exa client
│ │ ├── search.ts # Search wrappers
│ │ └── types.ts # Typed interfaces
│ └── index.ts
├── tests/
│ ├── exa.unit.test.ts # Mock-based unit tests
│ └── exa.integration.test.ts # Real API tests (needs key)
├── .env.local # Local secrets (git-ignored)
├── .env.example # Template for team
├── tsconfig.json
├── vitest.config.ts
└── package.json
{
"scripts": {
"dev": "tsx watch src/index.ts",
"test": "vitest",
"test:unit": "vitest --testPathPattern=unit",
"test:integration": "vitest --testPathPattern=integration",
"build": "tsc"
},
"dependencies": {
"exa-js": "^1.0.0"
},
"devDependencies": {
"tsx": "^4.0.0",
"vitest": "^2.0.0",
"typescript": "^5.0.0"
}
}
// tests/exa.unit.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock the exa-js module
vi.mock("exa-js", () => {
return {
default: vi.fn().mockImplementation(() => ({
search: vi.fn().mockResolvedValue({
results: [
{ url: "https://example.com/1", title: "Test Result 1", score: 0.95 },
{ url: "https://example.com/2", title: "Test Result 2", score: 0.87 },
],
}),
searchAndContents: vi.fn().mockResolvedValue({
results: [
{
url: "https://example.com/1",
title: "Test Result 1",
score: 0.95,
text: "This is the full text content of the page.",
highlights: ["Key excerpt from the page"],
summary: "A summary of the page content.",
},
],
}),
findSimilar: vi.fn().mockResolvedValue({
results: [
{ url: "https://similar.com/1", title: "Similar Page", score: 0.82 },
],
}),
getContents: vi.fn().mockResolvedValue({
results: [
{ url: "https://example.com/1", title: "Page", text: "Content" },
],
}),
})),
};
});
import Exa from "exa-js";
describe("Exa Search", () => {
let exa: any;
beforeEach(() => {
exa = new Exa("test-key");
});
it("should return search results", async () => {
const result = await exa.search("test query", { numResults: 5 });
expect(result.results).toHaveLength(2);
expect(result.results[0].score).toBeGreaterThan(0.9);
});
it("should return content with searchAndContents", async () => {
const result = await exa.searchAndContents("test", { text: true });
expect(result.results[0].text).toBeDefined();
expect(result.results[0].highlights).toHaveLength(1);
});
});
// tests/exa.integration.test.ts
import { describe, it, expect } from "vitest";
import Exa from "exa-js";
// Skip if no API key available (CI without secrets)
const describeWithKey = process.env.EXA_API_KEY
? describe
: describe.skip;
describeWithKey("Exa Integration", () => {
const exa = new Exa(process.env.EXA_API_KEY!);
it("should execute a basic search", async () => {
const result = await exa.search("test connectivity", { numResults: 1 });
expect(result.results.length).toBeGreaterThanOrEqual(1);
expect(result.results[0].url).toMatch(/^https?:\/\//);
}, 10000); // 10s timeout for API calls
it("should return text content", async () => {
const result = await exa.searchAndContents("TypeScript tutorial", {
numResults: 1,
text: { maxCharacters: 500 },
});
expect(result.results[0].text).toBeDefined();
expect(result.results[0].text!.length).toBeGreaterThan(0);
}, 15000);
it("should find similar pages", async () => {
const result = await exa.findSimilar("https://nodejs.org", {
numResults: 3,
});
expect(result.results.length).toBeGreaterThanOrEqual(1);
}, 10000);
});
set -euo pipefail
# Create .env.example template (commit this)
cat > .env.example << 'EOF'
# Exa API — get key at https://dashboard.exa.ai
EXA_API_KEY=
EOF
# Create local env (git-ignored)
cp .env.example .env.local
echo "EXA_API_KEY=your-key-here" > .env.local
| Error | Cause | Solution |
|---|---|---|
Cannot find module 'exa-js' | Not installed | Run npm install exa-js |
| Test timeout | Slow API response | Increase vitest timeout to 15000ms |
| Mock not applied | Import order issue | Ensure vi.mock() is before imports |
| Integration test fails in CI | No API key secret | Add EXA_API_KEY to CI secrets or skip |
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
testTimeout: 15000, // Exa API calls can take a few seconds
setupFiles: ["dotenv/config"],
},
});
See exa-sdk-patterns for production-ready code patterns.