From exa-pack
Configures Exa search API for dev, staging, prod with isolated API keys, per-env search limits, content settings, and optional Redis caching.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin exa-packThis skill is limited to using the following tools:
Exa charges per search request at `api.exa.ai`. Multi-environment setup focuses on API key isolation per environment, request limits and caching to control costs in staging, and appropriate `numResults`/content settings per tier.
Deploys Exa search API apps to Vercel Edge, Docker, and Cloud Run with endpoint templates, secret configs, and production bash commands.
Searches the web using Exa Search API via local Node script, returning structured results (title/URL/snippet/text). Useful when enabling Exa search, configuring EXA_API_KEY, or running Exa web searches.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Share bugs, ideas, or general feedback.
Exa charges per search request at api.exa.ai. Multi-environment setup focuses on API key isolation per environment, request limits and caching to control costs in staging, and appropriate numResults/content settings per tier.
exa-js installed (npm install exa-js)| Environment | Key Isolation | numResults | Content | Cache TTL |
|---|---|---|---|---|
| Development | Shared dev key | 3 | highlights only | None |
| Staging | Staging key | 5 | text (1000 chars) | 5 min |
| Production | Prod key | 10 | text (2000 chars) | 1 hour |
// config/exa.ts
import Exa from "exa-js";
type Env = "development" | "staging" | "production";
interface ExaEnvConfig {
apiKey: string;
defaultNumResults: number;
maxCharacters: number;
searchType: "auto" | "neural" | "keyword";
cacheEnabled: boolean;
cacheTtlSeconds: number;
}
const configs: Record<Env, Omit<ExaEnvConfig, "apiKey"> & { keyVar: string }> = {
development: {
keyVar: "EXA_API_KEY",
defaultNumResults: 3,
maxCharacters: 500,
searchType: "auto",
cacheEnabled: false,
cacheTtlSeconds: 0,
},
staging: {
keyVar: "EXA_API_KEY_STAGING",
defaultNumResults: 5,
maxCharacters: 1000,
searchType: "auto",
cacheEnabled: true,
cacheTtlSeconds: 300, // 5 minutes
},
production: {
keyVar: "EXA_API_KEY_PROD",
defaultNumResults: 10,
maxCharacters: 2000,
searchType: "neural",
cacheEnabled: true,
cacheTtlSeconds: 3600, // 1 hour
},
};
export function getExaConfig(): ExaEnvConfig {
const env = (process.env.NODE_ENV || "development") as Env;
const config = configs[env] || configs.development;
const apiKey = process.env[config.keyVar];
if (!apiKey) {
throw new Error(`${config.keyVar} not set for ${env} environment`);
}
return { ...config, apiKey };
}
export function getExaClient(): Exa {
return new Exa(getExaConfig().apiKey);
}
// lib/exa-search.ts
import { getExaClient, getExaConfig } from "../config/exa";
export async function search(query: string, numResults?: number) {
const exa = getExaClient();
const cfg = getExaConfig();
const n = numResults ?? cfg.defaultNumResults;
return exa.searchAndContents(query, {
type: cfg.searchType,
numResults: n,
text: { maxCharacters: cfg.maxCharacters },
});
}
// lib/exa-cache.ts
import { Redis } from "ioredis";
import { getExaClient, getExaConfig } from "../config/exa";
const redis = process.env.REDIS_URL ? new Redis(process.env.REDIS_URL) : null;
export async function cachedSearch(query: string, numResults?: number) {
const exa = getExaClient();
const cfg = getExaConfig();
const n = numResults ?? cfg.defaultNumResults;
if (cfg.cacheEnabled && redis) {
const cacheKey = `exa:${Buffer.from(`${query}:${n}:${cfg.searchType}`).toString("base64")}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const results = await exa.searchAndContents(query, {
type: cfg.searchType,
numResults: n,
text: { maxCharacters: cfg.maxCharacters },
});
await redis.set(cacheKey, JSON.stringify(results), "EX", cfg.cacheTtlSeconds);
return results;
}
return exa.searchAndContents(query, {
type: cfg.searchType,
numResults: n,
text: { maxCharacters: cfg.maxCharacters },
});
}
# .env.local (development)
EXA_API_KEY=exa-dev-key-here
# .env.staging
EXA_API_KEY_STAGING=exa-staging-key-here
REDIS_URL=redis://staging-redis:6379
# .env.production
EXA_API_KEY_PROD=exa-prod-key-here
REDIS_URL=redis://prod-redis:6379
# .github/workflows/deploy.yml
jobs:
deploy-staging:
environment: staging
env:
EXA_API_KEY_STAGING: ${{ secrets.EXA_API_KEY_STAGING }}
NODE_ENV: staging
steps:
- run: npm ci && npm run build && npm run deploy:staging
deploy-production:
environment: production
env:
EXA_API_KEY_PROD: ${{ secrets.EXA_API_KEY_PROD }}
NODE_ENV: production
steps:
- run: npm ci && npm run build && npm run deploy:prod
export async function checkExaHealth(): Promise<{
status: string;
env: string;
latencyMs: number;
}> {
const start = performance.now();
try {
const exa = getExaClient();
await exa.search("health check", { numResults: 1 });
return {
status: "healthy",
env: process.env.NODE_ENV || "development",
latencyMs: Math.round(performance.now() - start),
};
} catch {
return {
status: "unhealthy",
env: process.env.NODE_ENV || "development",
latencyMs: Math.round(performance.now() - start),
};
}
}
| Issue | Cause | Solution |
|---|---|---|
401 Unauthorized | Wrong API key for environment | Verify correct env var name |
429 rate_limit_exceeded | Too many requests | Enable caching and request queuing |
| High API costs in staging | No caching enabled | Enable Redis cache with 5-min TTL |
| Empty results in dev | numResults too low | Increase from 3 to 5 |
For deployment configuration, see exa-deploy-integration.