Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "exa-agent-skills",
"interface": {
"displayName": "Exa Agent Skills"
},
"plugins": [
{
"name": "agent-skills",
"version": "0.1.0",
"description": "Search, research, enrichment, and Exa API workflows.",
"source": {
"source": "local",
"path": "./"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
29 changes: 29 additions & 0 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "agent-skills",
"version": "0.1.0",
"description": "Exa workflows for search, content extraction, company research, lead generation, and API integration.",
"author": {
"name": "Exa Labs",
"url": "https://exa.ai"
},
"homepage": "https://github.com/exa-labs/agent-skills",
"repository": "https://github.com/exa-labs/agent-skills",
"skills": "./skills/",
"interface": {
"displayName": "Exa Agent Skills",
"shortDescription": "Search, research, enrichment, and Exa API workflows.",
"longDescription": "Reusable Exa workflows for semantic search, content extraction, company research, lead generation, and product integration.",
"developerName": "Exa Labs",
"category": "Productivity",
"capabilities": [
"Interactive",
"Read",
"Write"
],
"defaultPrompt": [
"Choose the right Exa workflow for this task",
"Research companies and return cited results",
"Help me integrate Exa into my application"
]
}
}
16 changes: 16 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Validate

on:
push:
pull_request:

jobs:
skills:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- run: node scripts/validate-repo.js
- run: node scripts/run-routing-evals.js
47 changes: 39 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,56 @@

[![skills.sh](https://skills.sh/b/exa-labs/agent-skills)](https://skills.sh/exa-labs/agent-skills)

Connect AI assistants to Exa's API skills: search, contents extraction, answer, Agent API, monitors, websets, and OpenAI-compatible endpoints.
Connect AI assistants to Exa's API skills: search, contents extraction, answer, Agent API, monitors, and OpenAI-compatible endpoints.

> [!NOTE]
> Get an API key from the [Exa Dashboard](https://dashboard.exa.ai/) and expose it as `EXA_API_KEY` to the agent environment.

> [!NOTE]
> You will need an API key to use the skills.
> You can get an API key from the [Exa Dashboard](https://dashboard.exa.ai/), then set it as `EXA_API_KEY` in your agent environment.
## Choose by task

## Available Skills
| I want to… | Use |
| --- | --- |
| Add Exa to an application, choose an endpoint, or debug an SDK/API request | `build-with-exa` |
| Search the web directly through `POST /search` without an SDK | `exa-search` |
| Extract content from URLs I already have through `POST /contents` | `exa-contents` |
| Research a company, market, competitors, funding, news, or leadership | `company-research` |
| Build and enrich an ICP-based prospect list or CSV | `lead-generation` |

```text
BUILD SEARCH EXTRACT RESEARCH PROSPECT
Integrate Exa Find URLs Read URLs Understand Build lists
```

| Skill | Description |
## Skills

| Skill | Boundary |
| --- | --- |
| `build-with-exa` | Build applications and agents with Exa's API Platform: search, contents extraction, answer, Agent API, monitors, websets, OpenAI-compatible endpoints, and `exa-py` / `exa-js`. |
| `build-with-exa` | Build applications and agents with Exa's API Platform: search, contents extraction, answer, Agent API, monitors, OpenAI-compatible endpoints, and `exa-py` / `exa-js`. |
| `exa-search` | Call Exa Search directly with cURL or raw HTTP for semantic web search, ranked results, content extraction, structured output, filters, freshness, and streaming search responses. |
| `exa-contents` | Call Exa Contents directly with cURL or raw HTTP for extracted text, highlights, summaries, links, image links, subpages, and freshness-controlled crawling from known URLs. |
| `company-research` | Research companies, competitors, funding, news, leadership, and market context with Exa Agent and advanced search. |
| `lead-generation` | Generate enriched ICP-based lead lists with Exa Agent, including structured scoring and CSV output. |

## Installation
Any agent supported by the Skills CLI:

```bash
npx skills add exa-labs/agent-skills
```

Codex plugin:

```bash
codex plugin marketplace add exa-labs/agent-skills
codex plugin add agent-skills@exa-agent-skills
```

The native plugin manifests point to the same root `skills/` tree. No agent-specific skill copies are maintained.

## Develop

```bash
node scripts/validate-repo.js
node scripts/run-routing-evals.js
```

See [docs/skill-anatomy.md](docs/skill-anatomy.md) for the lightweight contribution contract.
39 changes: 39 additions & 0 deletions docs/skill-anatomy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Skill Anatomy

Each skill lives at `skills/<name>/SKILL.md`. Keep the entrypoint concise and put conditional detail in a colocated `references/` directory.

## Required contract

- YAML frontmatter contains `name` and `description`.
- `name` is kebab-case and matches its directory.
- `description` says what the skill does and when to use it. It should distinguish nearby skills rather than list every feature.
- Instructions identify required tools or credentials when they are not universally available.
- Multi-step or artifact-producing workflows include evidence-based verification.
- Networked or asynchronous workflows explain terminal failures and bounded retry behavior.
- Local references stay inside the skill directory and are linked from `SKILL.md`.
- Instructions describe capabilities such as file writing, shell execution, browsers, and isolated subtasks without assuming a specific agent host.

## Recommended shape

Use only the sections that improve behavior. Common choices are:

- Overview or scope
- When to use and when not to use
- Prerequisites or tool selection
- Workflow
- Failure handling
- Red flags or common mistakes
- Output contract
- Verification

Equivalent headings are fine. Do not add empty sections or a large template to satisfy appearances.

## Progressive disclosure

Keep shared decisions and constraints in `SKILL.md`. Move substantial endpoint details, schemas, or mode-specific examples into `skills/<name>/references/`. Link references directly and say when they matter.

Do not create repository-root shared references. A skill installed on its own must carry everything it needs.

## Verification standard

Verification must name observable evidence: a valid response shape, terminal run state, cited sources, requested row count, a parseable artifact, or a successful request. “Looks correct” is not verification.
44 changes: 44 additions & 0 deletions evals/routing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"cases": [
{
"prompt": "Integrate Exa semantic search into my TypeScript application using the SDK",
"expected": "build-with-exa"
},
{
"prompt": "Which Exa API endpoint and request shape should my product use for research?",
"expected": "build-with-exa"
},
{
"prompt": "Call POST /search directly with curl and filter results by published date",
"expected": "exa-search"
},
{
"prompt": "Run a semantic web search and return ranked results with highlights",
"expected": "exa-search"
},
{
"prompt": "I already have these URLs; extract their text, links, and subpages",
"expected": "exa-contents"
},
{
"prompt": "Use POST /contents directly to get freshness-controlled highlights from known URLs",
"expected": "exa-contents"
},
{
"prompt": "Research this company, its funding, executives, recent news, and competitors",
"expected": "company-research"
},
{
"prompt": "Do a competitor analysis and market deep dive on Anthropic",
"expected": "company-research"
},
{
"prompt": "Build a CSV prospect list of 100 companies matching our ICP",
"expected": "lead-generation"
},
{
"prompt": "Find sales leads, enrich them with buying signals, and score ICP fit",
"expected": "lead-generation"
}
]
}
74 changes: 74 additions & 0 deletions scripts/run-routing-evals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env node

'use strict';

const fs = require('fs');
const path = require('path');

const ROOT = path.resolve(__dirname, '..');
const STOP_WORDS = new Set([
'a', 'an', 'and', 'are', 'by', 'for', 'from', 'i', 'in', 'into', 'is', 'it', 'my',
'of', 'on', 'or', 'our', 'the', 'their', 'these', 'this', 'to', 'use', 'using', 'with'
]);

function tokenize(value) {
return value.toLowerCase()
.replace(/[^a-z0-9]+/g, ' ')
.split(/\s+/)
.filter(Boolean)
.filter((token) => !STOP_WORDS.has(token))
.map((token) => token
.replace(/ies$/, 'y')
.replace(/ing$/, '')
.replace(/ed$/, '')
.replace(/s$/, ''));
}

function descriptionFor(skillName) {
const content = fs.readFileSync(path.join(ROOT, 'skills', skillName, 'SKILL.md'), 'utf8');
const match = content.match(/^description:\s*["']?(.+?)["']?\s*$/m);
if (!match) throw new Error(`${skillName}: missing one-line description`);
return match[1];
}

function score(promptTokens, documentTokens) {
const counts = new Map();
for (const token of documentTokens) counts.set(token, (counts.get(token) || 0) + 1);
return promptTokens.reduce((total, token) => total + Math.min(counts.get(token) || 0, 2), 0);
}

function rank(prompt, skillNames) {
const promptTokens = tokenize(prompt);
return skillNames.map((skillName) => ({
skillName,
score: score(promptTokens, tokenize(`${skillName} ${descriptionFor(skillName)}`))
})).sort((a, b) => b.score - a.score || a.skillName.localeCompare(b.skillName));
}

function main() {
const config = JSON.parse(fs.readFileSync(path.join(ROOT, 'evals', 'routing.json'), 'utf8'));
const skillNames = fs.readdirSync(path.join(ROOT, 'skills'))
.filter((name) => fs.statSync(path.join(ROOT, 'skills', name)).isDirectory())
.sort();
const failures = [];

for (const testCase of config.cases) {
const results = rank(testCase.prompt, skillNames);
const winner = results[0];
if (winner.skillName !== testCase.expected || winner.score === 0) {
failures.push(`${JSON.stringify(testCase.prompt)} expected ${testCase.expected}, got ${winner.skillName} (${winner.score})`);
}
}

if (failures.length) {
for (const failure of failures) console.error(`FAIL: ${failure}`);
console.error(`\n${failures.length}/${config.cases.length} routing eval(s) failed.`);
process.exit(1);
}

console.log(`${config.cases.length} routing evals passed.`);
}

if (require.main === module) main();

module.exports = { rank, tokenize };
Loading
Loading