migrate: consolidate all skills and agents from ~/clawd
- Moved 4 skills: browser-testing, claude-multimedia, exa-search, gravl-research - Moved 14 agents: architect, backend-dev, browser-tester, coach, data, flight, frontend-dev, gravl-pm, gravl-researcher, nutritionist, research, reviewer, staging, update - Created symlinks from ~/clawd/skills and ~/clawd/agents back to hub - Single source of truth in claude-agents-skills repo
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
# Browser Testing Skill
|
||||
|
||||
Automatisk webbtestning med headless Chrome för Gravl och andra projekt.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Installera Playwright (enklast)
|
||||
npm install -g playwright
|
||||
npx playwright install chromium
|
||||
|
||||
# Eller Puppeteer
|
||||
npm install -g puppeteer
|
||||
|
||||
# Eller direkt Chrome
|
||||
which chromium-browser || which google-chrome || which chromium
|
||||
```
|
||||
|
||||
## Snabbstart
|
||||
|
||||
### 1. Skärmdump för visuell regression
|
||||
|
||||
```javascript
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.goto('https://03-design-polish.gravl.homelab.local');
|
||||
await page.screenshot({ path: 'gravl-landing.png', fullPage: true });
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
```
|
||||
|
||||
### 2. Interaktivt test (login-flöde)
|
||||
|
||||
```javascript
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
// Gå till login
|
||||
await page.goto('https://03-design-polish.gravl.homelab.local/login');
|
||||
|
||||
// Fyll i formulär
|
||||
await page.fill('[data-testid="email"]', 'test@example.com');
|
||||
await page.fill('[data-testid="password"]', 'test123');
|
||||
|
||||
// Klicka login
|
||||
await page.click('[data-testid="login-button"]');
|
||||
|
||||
// Vänta på redirect
|
||||
await page.waitForURL('**/dashboard');
|
||||
|
||||
// Verifiera
|
||||
const welcomeText = await page.textContent('[data-testid="welcome-message"]');
|
||||
console.log('Login test:', welcomeText.includes('Välkommen') ? '✅ PASS' : '❌ FAIL');
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
```
|
||||
|
||||
## Gravl-specifika tester
|
||||
|
||||
### Test Suite: Login/Onboarding (Phase 3)
|
||||
|
||||
```javascript
|
||||
// tests/gravl-phase3.spec.js
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Gravl Phase 3 - Login/Onboarding', () => {
|
||||
const BASE_URL = process.env.STAGING_URL || 'https://03-design-polish.gravl.homelab.local';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(BASE_URL);
|
||||
});
|
||||
|
||||
test('visar logotyp', async ({ page }) => {
|
||||
const logo = await page.locator('[data-testid="logo"]').isVisible();
|
||||
expect(logo).toBeTruthy();
|
||||
});
|
||||
|
||||
test('login-form finns', async ({ page }) => {
|
||||
await expect(page.locator('form')).toBeVisible();
|
||||
await expect(page.locator('input[type="email"]')).toBeVisible();
|
||||
await expect(page.locator('input[type="password"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('gradient animationer', async ({ page }) => {
|
||||
const body = await page.locator('body');
|
||||
const bg = await body.evaluate(el =>
|
||||
getComputedStyle(el).background
|
||||
);
|
||||
expect(bg).toContain('gradient');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Test Suite: Dashboard (Phase 3)
|
||||
|
||||
```javascript
|
||||
test.describe('Gravl Phase 3 - Dashboard', () => {
|
||||
test('stat cards visas', async ({ page }) => {
|
||||
await page.goto(process.env.STAGING_URL + '/dashboard');
|
||||
const cards = await page.locator('[data-testid="stat-card"]').count();
|
||||
expect(cards).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('kalender komponent', async ({ page }) => {
|
||||
const calendar = await page.locator('[data-testid="calendar"]').isVisible();
|
||||
expect(calendar).toBeTruthy();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Test Suite: Workout Page (Phase 3)
|
||||
|
||||
```javascript
|
||||
test.describe('Gravl Phase 3 - Workout', () => {
|
||||
test('exercise cards med animationer', async ({ page }) => {
|
||||
await page.goto(process.env.STAGING_URL + '/workout');
|
||||
const cards = await page.locator('.exercise-card');
|
||||
await expect(cards.first()).toHaveClass(/animate/);
|
||||
});
|
||||
|
||||
test('rest timer finns', async ({ page }) => {
|
||||
const timer = await page.locator('[data-testid="rest-timer"]').isVisible();
|
||||
expect(timer).toBeTruthy();
|
||||
});
|
||||
|
||||
test('KLART button styling', async ({ page }) => {
|
||||
const button = page.locator('[data-testid="complete-workout"]');
|
||||
await expect(button).toHaveCSS('background-color', 'rgb(255, 107, 53)');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Automatisk staging-test
|
||||
|
||||
### Skript för PM
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# /workspace/gravl/scripts/test-staging.sh
|
||||
|
||||
BRANCH_NAME=$1
|
||||
echo "🧪 Testar staging för $BRANCH_NAME"
|
||||
|
||||
export STAGING_URL="https://$BRANCH_NAME.gravl.homelab.local"
|
||||
|
||||
# Kör Playwright-tester
|
||||
cd /workspace/gravl/frontend
|
||||
npx playwright test tests/gravl-phase3.spec.js --reporter=json
|
||||
|
||||
# Om tester failar → rapportera till PM
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Tester failade"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Alla tester passerade"
|
||||
```
|
||||
|
||||
## PM Workflow med browser-test
|
||||
|
||||
### Steg 1: Efter staging skapats
|
||||
|
||||
```bash
|
||||
# PM spawnar test-agent
|
||||
sessions_spawn:
|
||||
agentId: browser-tester
|
||||
task: "Testa https://03-design-polish.gravl.homelab.local.
|
||||
Använd Playwright.
|
||||
Kolla: login-form, logotyp, animationer, rest-timer.
|
||||
Rapportera pass/fail för varje test."
|
||||
```
|
||||
|
||||
### Steg 2: Test-agenten kör
|
||||
|
||||
```bash
|
||||
exec pty:true workdir:/workspace/gravl \
|
||||
command:"npx playwright test --reporter=html"
|
||||
```
|
||||
|
||||
### Steg 3: Rapportera resultat
|
||||
|
||||
```
|
||||
🧪 Browser Test Results
|
||||
|
||||
✅ Login page (3/3)
|
||||
- Logotyp visible
|
||||
- Form validation
|
||||
- Gradient animations
|
||||
|
||||
✅ Dashboard (2/2)
|
||||
- Stat cards
|
||||
- Calendar component
|
||||
|
||||
✅ Workout (3/3)
|
||||
- Exercise cards
|
||||
- Rest timer
|
||||
- KLART button
|
||||
|
||||
📊 Screenshot: test-results/screenshots/
|
||||
📄 Report: test-results/report.html
|
||||
|
||||
Alla Phase 3-tester passerade! ✅
|
||||
```
|
||||
|
||||
## Headless Chrome direkt
|
||||
|
||||
### Utan Playwright (bara Chrome)
|
||||
|
||||
```bash
|
||||
# Skärmdump
|
||||
chromium --headless --disable-gpu --screenshot=gravl.png \
|
||||
--window-size=1920,1080 \
|
||||
https://03-design-polish.gravl.homelab.local
|
||||
|
||||
# PDF-export
|
||||
chromium --headless --disable-gpu --print-to-pdf=gravl.pdf \
|
||||
https://03-design-polish.gravl.homelab.local
|
||||
|
||||
# HTML-innehåll
|
||||
chromium --headless --disable-gpu --dump-dom \
|
||||
https://03-design-polish.gravl.homelab.local > gravl.html
|
||||
```
|
||||
|
||||
## Skill-definition (för OpenClaw)
|
||||
|
||||
```yaml
|
||||
name: browser-testing
|
||||
version: 1.0.0
|
||||
commands:
|
||||
capture:
|
||||
exec: "chromium --headless --screenshot={output} {url}"
|
||||
test:
|
||||
exec: "npx playwright test {spec} --reporter={format}"
|
||||
interactive:
|
||||
exec: "npx playwright open {url}"
|
||||
env:
|
||||
CHROME_BIN: /usr/bin/chromium
|
||||
PLAYWRIGHT_BROWSERS_PATH: 0
|
||||
```
|
||||
|
||||
## Installation i Gravl
|
||||
|
||||
```bash
|
||||
cd /workspace/gravl
|
||||
npm init -y
|
||||
npm install --save-dev @playwright/test
|
||||
npx playwright install chromium
|
||||
|
||||
# Skapa test-mapp
|
||||
mkdir -p tests
|
||||
mkdir -p test-results/screenshots
|
||||
```
|
||||
|
||||
## Integrering med PM
|
||||
|
||||
```javascript
|
||||
// I gravl-pm/SOUL.md
|
||||
|
||||
## Browser Testing
|
||||
|
||||
Efter varje staging-creation:
|
||||
|
||||
1. Skärmdump för visuell validering
|
||||
2. Kärnflödestest (login -> dashboard)
|
||||
3. Rapportera till Josef med screenshots
|
||||
|
||||
Använd alltid:
|
||||
- exec pty:true för Playwright
|
||||
- Spara screenshots i .staging/{branch}/screenshots/
|
||||
- Vid fail → rapportera med screenshot
|
||||
```
|
||||
|
||||
## Resurser
|
||||
|
||||
- [Playwright Docs](https://playwright.dev/)
|
||||
- [Headless Chrome](https://developer.chrome.com/docs/chromium/headless)
|
||||
- [Testing Best Practices](https://playwright.dev/docs/best-practices)
|
||||
@@ -0,0 +1,346 @@
|
||||
# Claude Multimedia Skill
|
||||
|
||||
Generera bilder (Gemini 3 Pro Image/Nano Banana Pro) och videor (Veo) direkt från Claude Code.
|
||||
|
||||
## Installation
|
||||
|
||||
**Google Cloud Setup (för Veo videogenerering):**
|
||||
|
||||
```bash
|
||||
# 1. Installera Google Cloud CLI
|
||||
curl https://sdk.cloud.google.com | bash
|
||||
exec -l $SHELL
|
||||
gcloud init
|
||||
|
||||
# 2. Login
|
||||
gcloud auth application-default login
|
||||
|
||||
# 3. Enable APIs
|
||||
gcloud services enable videogeneration.googleapis.com
|
||||
gcloud services enable aiplatform.googleapis.com
|
||||
|
||||
# 4. Set projekt
|
||||
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
|
||||
export GOOGLE_APPLICATION_CREDENTIALS=~/.config/gcloud/application_default_credentials.json
|
||||
```
|
||||
|
||||
**Nano Banana Pro (redan installerat):**
|
||||
```bash
|
||||
openclaw skills # Shows nano-banana-pro ✓ ready
|
||||
```
|
||||
|
||||
## Använda från Claude Code
|
||||
|
||||
### 1. Generera bilder med Nano Banana Pro
|
||||
|
||||
```javascript
|
||||
// Image generation via CLI
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Enkelt sätt
|
||||
function generateImage(prompt, filename) {
|
||||
const cmd = `nano-banana-pro generate "${prompt}" --output ${filename}`;
|
||||
execSync(cmd);
|
||||
return filename;
|
||||
}
|
||||
|
||||
// Exempel
|
||||
const imageFile = generateImage(
|
||||
'Beautiful Gravl app dashboard with dark theme, animated stat cards, gradient background',
|
||||
'gravl-dashboard.png'
|
||||
);
|
||||
console.log('Image created:', imageFile);
|
||||
```
|
||||
|
||||
### 2. Generera videor med Veo
|
||||
|
||||
```javascript
|
||||
function generateVideo(prompt, filename, duration = 5) {
|
||||
const cmd = `veo generate "${prompt}" --output ${filename} --duration ${duration}`;
|
||||
execSync(cmd);
|
||||
return filename;
|
||||
}
|
||||
|
||||
// Exempel: Animerad workout-demo
|
||||
const videoFile = generateVideo(
|
||||
'User doing a workout in Gravl app: clicking exercises, adding sets, using rest timer',
|
||||
'gravl-workout-demo.mp4',
|
||||
10
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Batch-generering
|
||||
|
||||
```javascript
|
||||
// Batch av bilder för Gravl design-system
|
||||
const designs = [
|
||||
{ name: 'login-page', prompt: 'Gravl login page with logo, email/password inputs, gradient background' },
|
||||
{ name: 'dashboard', prompt: 'Gravl dashboard with stat cards, calendar, upcoming workouts' },
|
||||
{ name: 'workout', prompt: 'Gravl workout page with exercise cards, rest timer, complete button' }
|
||||
];
|
||||
|
||||
designs.forEach(({ name, prompt }) => {
|
||||
generateImage(prompt, `designs/${name}.png`);
|
||||
});
|
||||
```
|
||||
|
||||
## API-stil (om CLI inte fungerar)
|
||||
|
||||
### Via gemini API (Nano Banana Pro)
|
||||
|
||||
```javascript
|
||||
const Anthropic = require('@anthropic-ai/sdk');
|
||||
|
||||
async function generateImageViaAPI(prompt) {
|
||||
const client = new Anthropic();
|
||||
|
||||
const message = await client.messages.create({
|
||||
model: "google/gemini-3-pro-vision",
|
||||
max_tokens: 1024,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Generate an image of: ${prompt}`
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return message.content[0];
|
||||
}
|
||||
|
||||
// Använd det
|
||||
const image = await generateImageViaAPI('Gravl app with dark theme and orange accents');
|
||||
```
|
||||
|
||||
### Via Google Veo API (Vertex AI)
|
||||
|
||||
```javascript
|
||||
const { VideoGenerationServiceClient } = require('@google-cloud/videogeneration');
|
||||
|
||||
async function generateVideoWithGoogleVeo(prompt, duration = 10) {
|
||||
const client = new VideoGenerationServiceClient({
|
||||
projectId: process.env.GOOGLE_CLOUD_PROJECT,
|
||||
keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS
|
||||
});
|
||||
|
||||
const request = {
|
||||
parent: `projects/${process.env.GOOGLE_CLOUD_PROJECT}/locations/us-central1`,
|
||||
videoGenerationConfig: {
|
||||
prompt: prompt,
|
||||
duration: `${duration}s`,
|
||||
resolution: '1080p'
|
||||
}
|
||||
};
|
||||
|
||||
const operation = await client.generateVideo(request);
|
||||
const [response] = await operation.promise();
|
||||
|
||||
return response.videoUri;
|
||||
}
|
||||
|
||||
// Använd det
|
||||
const videoUri = await generateVideoWithGoogleVeo(
|
||||
'Gravl fitness app demo: user logs workout with sets and reps, uses rest timer',
|
||||
10
|
||||
);
|
||||
```
|
||||
|
||||
## Graviering-use-cases
|
||||
|
||||
### 1. Design-system bilder
|
||||
|
||||
```javascript
|
||||
// Gravl design-system gallery
|
||||
const components = [
|
||||
{ type: 'button', prompt: 'Gravl button component in orange (#ff6b35), dark background' },
|
||||
{ type: 'card', prompt: 'Gravl stat card component with gradient, animation effect' },
|
||||
{ type: 'input', prompt: 'Gravl input field with focus state, validation states' }
|
||||
];
|
||||
|
||||
for (const { type, prompt } of components) {
|
||||
generateImage(prompt, `design-system/${type}.png`);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Marketing-videor
|
||||
|
||||
```javascript
|
||||
// Workout demo-video
|
||||
generateVideo(
|
||||
'Gravl fitness app demo: user opens app, selects workout, logs exercise with sets/reps, waits for rest timer, completes workout, sees progress stats',
|
||||
'gravl-demo.mp4',
|
||||
15
|
||||
);
|
||||
|
||||
// Feature showcase
|
||||
generateVideo(
|
||||
'Gravl phase-3 features: beautiful login screen with logo, dashboard with animated stat cards, workout page with rest timer countdown',
|
||||
'gravl-features-showcase.mp4',
|
||||
20
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Landing-page assets
|
||||
|
||||
```javascript
|
||||
// Hero image för gravl.com
|
||||
generateImage(
|
||||
'Hero image for fitness app: user in gym with phone showing Gravl app, dark modern aesthetic, orange and blue accents, 1920x1080',
|
||||
'hero-image.png'
|
||||
);
|
||||
|
||||
// Features section
|
||||
generateImage(
|
||||
'Illustration of workout logging: hand touching phone screen, exercise cards, strength progression',
|
||||
'feature-logging.png'
|
||||
);
|
||||
|
||||
generateImage(
|
||||
'Illustration of rest timer: clock animation, countdown, energy/recovery visualization',
|
||||
'feature-timer.png'
|
||||
);
|
||||
```
|
||||
|
||||
## Integration med PM/Staging
|
||||
|
||||
### Använd från gravl-pm
|
||||
|
||||
```bash
|
||||
# PM spawnar en rendering-agent
|
||||
sessions_spawn:
|
||||
agentId: claude-code
|
||||
task: "Använd claude-multimedia skill för att:
|
||||
1. Generera 3 design-system bilder för Gravl
|
||||
2. Skapa en 10-sekunders demo-video av workout-flödet
|
||||
|
||||
Använd nano-banana-pro för bilder, veo för video.
|
||||
Spara till /workspace/gravl/marketing/
|
||||
Rapportera filer när klart."
|
||||
timeoutSeconds: 300
|
||||
```
|
||||
|
||||
### Innan staging-merge
|
||||
|
||||
```bash
|
||||
# Skapa automatiska marketing-assets
|
||||
exec pty:true workdir:/workspace/gravl command:"claude 'Använd claude-multimedia för:
|
||||
- 5 design-system component-bilder
|
||||
- 1 workout demo-video (15s)
|
||||
- 1 feature showcase-video (20s)
|
||||
Spara till marketing/assets/'"
|
||||
```
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Environment variables
|
||||
|
||||
```bash
|
||||
# .env
|
||||
export GOOGLE_APPLICATION_CREDENTIALS=~/.config/gcloud/application_default_credentials.json
|
||||
export GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
export GRAVL_MARKETING_DIR="/workspace/gravl/marketing"
|
||||
```
|
||||
|
||||
### npx-kommando
|
||||
|
||||
```bash
|
||||
# Lägg till i package.json scripts
|
||||
"scripts": {
|
||||
"generate:images": "node scripts/generate-images.js",
|
||||
"generate:videos": "node scripts/generate-videos.js",
|
||||
"generate:all": "npm run generate:images && npm run generate:videos"
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced: Prompt-templates
|
||||
|
||||
### Design-system prompt template
|
||||
|
||||
```javascript
|
||||
const designSystemPrompt = (component, style) => `
|
||||
Gravl fitness app ${component} component.
|
||||
Style: ${style}
|
||||
Design: Dark theme (#0a0a0f), Orange accent (#ff6b35),
|
||||
Modern minimalist, Mobile-first,
|
||||
High contrast, Professional
|
||||
Resolution: 1280x720px
|
||||
`;
|
||||
|
||||
// Använd det
|
||||
generateImage(designSystemPrompt('stat-card', 'animated'), 'stat-card.png');
|
||||
```
|
||||
|
||||
### Video prompt template
|
||||
|
||||
```javascript
|
||||
const videoPrompt = (scenario, duration) => `
|
||||
Gravl fitness app: ${scenario}
|
||||
Style: Modern, smooth animations, dark theme with orange accents
|
||||
Duration: ${duration}s
|
||||
Quality: 1080p cinematic
|
||||
Music: subtle, motivational
|
||||
`;
|
||||
|
||||
// Använd det
|
||||
generateVideo(
|
||||
videoPrompt('user completes workout and sees achievement celebration', 10),
|
||||
'celebration-demo.mp4'
|
||||
);
|
||||
```
|
||||
|
||||
## Batch-verktygScript
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/generate-all-assets.sh
|
||||
|
||||
mkdir -p marketing/images
|
||||
mkdir -p marketing/videos
|
||||
|
||||
# Generera design-system
|
||||
echo "Generating design-system images..."
|
||||
node scripts/generate-images.js
|
||||
|
||||
# Generera videos
|
||||
echo "Generating demo videos..."
|
||||
node scripts/generate-videos.js
|
||||
|
||||
# Zip allt
|
||||
cd marketing
|
||||
zip -r gravl-assets.zip images/ videos/
|
||||
echo "✅ Assets packaged: gravl-assets.zip"
|
||||
```
|
||||
|
||||
## Felsökning
|
||||
|
||||
| Problem | Lösning |
|
||||
|---------|---------|
|
||||
| `Command not found: nano-banana-pro` | `openclaw clawhub install nano-banana-pro` |
|
||||
| `GEMINI_API_KEY not set` | `export GEMINI_API_KEY=your-key` |
|
||||
| `VEO rate limit` | Använd queue/batch-mode, vänta mellan requests |
|
||||
| Timeout på video-gen | Öka `timeoutSeconds` i PM-spawning (30+ min för long videos) |
|
||||
|
||||
## Resurser
|
||||
|
||||
- [Nano Banana Pro Skill](/home/linuxbrew/.linuxbrew/lib/node_modules/openclaw/skills/nano-banana-pro/SKILL.md)
|
||||
- [Gemini 3 Pro Image API](https://cloud.google.com/vertex-ai/docs/generative-ai/image/generate-images)
|
||||
- [Google Veo Video API (Vertex AI)](https://cloud.google.com/vertex-ai/docs/generative-ai/video/generate-videos)
|
||||
- [Google Cloud Setup](https://cloud.google.com/docs/authentication/application-default-credentials)
|
||||
- [Design-system best practices](https://design.gravl.app)
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Cache images** — Spara genererade bilder för att undvika duplikat-API-calls
|
||||
2. **Use templates** — Standardisera prompts för konsistent stil
|
||||
3. **Batch processing** — Köra flera genereringar i parallell
|
||||
4. **Version tracking** — Lagra assets i git-lfs för stora filer
|
||||
5. **Approval flow** — Spara till staging-folder, kolla innan prod
|
||||
|
||||
---
|
||||
|
||||
Skapad för Gravl project med focus på marketing-assets och design-system visualisering.
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: exa-search
|
||||
description: Web search, code search, company research, and deep research via Exa AI. Use for real-time web queries, finding code examples, researching companies, finding people, or starting deep research tasks. Triggers on web search needs, code lookups, company/people research.
|
||||
---
|
||||
|
||||
# Exa Search
|
||||
|
||||
Exa AI search via their hosted MCP server. Provides web search, code context, company research, people search, and deep research capabilities.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Web search
|
||||
~/clawd/skills/exa-search/scripts/exa-cli.mjs search "latest AI agent frameworks 2026"
|
||||
|
||||
# Code search
|
||||
~/clawd/skills/exa-search/scripts/exa-cli.mjs code "Python async retry with exponential backoff"
|
||||
|
||||
# Company research
|
||||
~/clawd/skills/exa-search/scripts/exa-cli.mjs company "Anthropic AI"
|
||||
|
||||
# People search
|
||||
~/clawd/skills/exa-search/scripts/exa-cli.mjs people "CTO machine learning startups Stockholm"
|
||||
|
||||
# Crawl specific URL
|
||||
~/clawd/skills/exa-search/scripts/exa-cli.mjs crawl "https://example.com/article"
|
||||
|
||||
# Deep research (async)
|
||||
~/clawd/skills/exa-search/scripts/exa-cli.mjs research "State of quantum computing 2026"
|
||||
~/clawd/skills/exa-search/scripts/exa-cli.mjs research-check <task-id>
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool | Use Case |
|
||||
|------|----------|
|
||||
| `search` | General web search, news, current events |
|
||||
| `search-advanced` | Filtered search (domains, dates, content type) |
|
||||
| `code` | Code examples, docs, GitHub, StackOverflow |
|
||||
| `crawl` | Full content from specific URL |
|
||||
| `company` | Company info, news, competitors |
|
||||
| `people` | LinkedIn profiles, professional bios |
|
||||
| `research` | Start deep AI research task |
|
||||
| `research-check` | Check research task status |
|
||||
|
||||
## API Key (Optional)
|
||||
|
||||
Free tier has rate limits. Add your key for higher limits:
|
||||
|
||||
```bash
|
||||
export EXA_API_KEY="your-key-here"
|
||||
```
|
||||
|
||||
Get key: https://dashboard.exa.ai/api-keys
|
||||
|
||||
## Advanced Search Options
|
||||
|
||||
```bash
|
||||
# Filter by domain
|
||||
exa-cli.mjs search-advanced "AI news" --domains techcrunch.com,wired.com
|
||||
|
||||
# Date range
|
||||
exa-cli.mjs search-advanced "OpenAI announcements" --after 2026-01-01
|
||||
|
||||
# More results
|
||||
exa-cli.mjs search "topic" --num 20
|
||||
```
|
||||
|
||||
## Tool Details
|
||||
|
||||
See [references/tools.md](references/tools.md) for full parameter documentation.
|
||||
|
||||
## Tips
|
||||
|
||||
- **Code search**: Include language name ("Python", "Go", "TypeScript") for better results
|
||||
- **Company research**: Start with company name, then drill into specifics
|
||||
- **Deep research**: Returns task ID; poll with `research-check` until complete
|
||||
- **Rate limits**: Free tier ~1 req/sec; add API key for more
|
||||
@@ -0,0 +1,211 @@
|
||||
# Exa MCP Tools Reference
|
||||
|
||||
## web_search_exa
|
||||
|
||||
Basic web search with clean, ready-to-use content.
|
||||
|
||||
**Parameters:**
|
||||
- `query` (string, required): Search query
|
||||
- `numResults` (number, optional): Number of results (default: 10)
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
exa-cli.mjs search "AI agent frameworks 2026"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## web_search_advanced_exa
|
||||
|
||||
Advanced search with full control over filters.
|
||||
|
||||
**Parameters:**
|
||||
- `query` (string, required): Search query
|
||||
- `numResults` (number, optional): Number of results
|
||||
- `type` (string, optional): "auto", "neural", "deep"
|
||||
- `category` (string, optional): "company", "news", "tweet", "people", "personal site"
|
||||
- `includeDomains` (array, optional): Only include these domains
|
||||
- `excludeDomains` (array, optional): Exclude these domains
|
||||
- `startPublishedDate` (string, optional): ISO date YYYY-MM-DD
|
||||
- `endPublishedDate` (string, optional): ISO date YYYY-MM-DD
|
||||
- `livecrawl` (string, optional): "never", "fallback", "always"
|
||||
|
||||
**Category Restrictions:**
|
||||
- `category: "company"`: Cannot use domain or date filters
|
||||
- `category: "people"`: Cannot use date filters, excludeDomains, or excludeText
|
||||
- `includeText`/`excludeText`: Only single-item arrays
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Domain filter
|
||||
exa-cli.mjs search-advanced "AI news" --domains techcrunch.com,wired.com
|
||||
|
||||
# Date filter
|
||||
exa-cli.mjs search-advanced "OpenAI" --after 2026-01-01 --before 2026-02-01
|
||||
|
||||
# More results
|
||||
exa-cli.mjs search-advanced "machine learning" --num 30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## get_code_context_exa
|
||||
|
||||
Find code examples, documentation, and solutions from GitHub, StackOverflow, and technical docs.
|
||||
|
||||
**Parameters:**
|
||||
- `query` (string, required): Code-related query
|
||||
- `tokensNum` (number, optional): Token limit (default: 5000, range: 1000-50000)
|
||||
|
||||
**Best Practices:**
|
||||
- Always include programming language in query
|
||||
- Include framework + version when relevant
|
||||
- Use exact identifiers (function names, error messages)
|
||||
|
||||
**Token Strategy:**
|
||||
- Focused snippet: 1000-3000
|
||||
- Most tasks: 5000
|
||||
- Complex integration: 10000-20000
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Python specific
|
||||
exa-cli.mjs code "Python asyncio retry exponential backoff"
|
||||
|
||||
# Framework specific
|
||||
exa-cli.mjs code "Next.js 14 server actions form handling"
|
||||
|
||||
# Error lookup
|
||||
exa-cli.mjs code "ECONNREFUSED Node.js Docker"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## crawling_exa
|
||||
|
||||
Get full content from a specific URL.
|
||||
|
||||
**Parameters:**
|
||||
- `url` (string, required): URL to crawl
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
exa-cli.mjs crawl "https://docs.anthropic.com/claude/docs"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## company_research_exa
|
||||
|
||||
Research any company for business information, news, and insights.
|
||||
|
||||
**Parameters:**
|
||||
- `companyName` (string, required): Company name to research
|
||||
|
||||
**Returns:**
|
||||
- Company overview
|
||||
- Recent news
|
||||
- Key metrics (when available)
|
||||
- Competitors
|
||||
- Funding info
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
exa-cli.mjs company "Stripe"
|
||||
exa-cli.mjs company "Klarna fintech"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## people_search_exa
|
||||
|
||||
Find people and their professional profiles.
|
||||
|
||||
**Parameters:**
|
||||
- `query` (string, required): Search query (role, name, company, etc.)
|
||||
- `numResults` (number, optional): Number of results
|
||||
|
||||
**Notes:**
|
||||
- Returns public LinkedIn profiles and professional bios
|
||||
- Works best with role + location/company
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
exa-cli.mjs people "VP Engineering AI startups San Francisco"
|
||||
exa-cli.mjs people "CTO fintech Stockholm"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## deep_researcher_start
|
||||
|
||||
Start an AI research agent that searches, reads, and writes a detailed report.
|
||||
|
||||
**Parameters:**
|
||||
- `query` (string, required): Research topic
|
||||
|
||||
**Returns:**
|
||||
- `taskId`: Use with `research-check` to get results
|
||||
|
||||
**Note:** Deep research is async. Poll with `research-check` until status is "completed".
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
exa-cli.mjs research "Current state of nuclear fusion energy startups 2026"
|
||||
# Returns: taskId: abc123...
|
||||
|
||||
exa-cli.mjs research-check abc123
|
||||
# Poll until complete
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## deep_researcher_check
|
||||
|
||||
Check status and get results from a deep research task.
|
||||
|
||||
**Parameters:**
|
||||
- `taskId` (string, required): Task ID from `deep_researcher_start`
|
||||
|
||||
**Status Values:**
|
||||
- `pending`: Still processing
|
||||
- `completed`: Results ready
|
||||
- `failed`: Error occurred
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
exa-cli.mjs research-check abc123-def456-...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rate Limits
|
||||
|
||||
**Free Plan:**
|
||||
- ~1 request/second
|
||||
- 2000 requests/month
|
||||
|
||||
**With API Key:**
|
||||
- Higher limits based on plan
|
||||
- Get key: https://dashboard.exa.ai/api-keys
|
||||
|
||||
Set key:
|
||||
```bash
|
||||
export EXA_API_KEY="your-key-here"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MCP Server URL
|
||||
|
||||
Base: `https://mcp.exa.ai/mcp`
|
||||
|
||||
With all tools:
|
||||
```
|
||||
https://mcp.exa.ai/mcp?tools=web_search_exa,web_search_advanced_exa,get_code_context_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check
|
||||
```
|
||||
|
||||
With API key:
|
||||
```
|
||||
https://mcp.exa.ai/mcp?exaApiKey=YOUR_KEY&tools=...
|
||||
```
|
||||
Executable
+221
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Exa Search CLI - Wrapper for Exa MCP Server
|
||||
*
|
||||
* Usage:
|
||||
* ./exa-cli.mjs search "query"
|
||||
* ./exa-cli.mjs code "Python async patterns"
|
||||
* ./exa-cli.mjs company "Anthropic"
|
||||
* ./exa-cli.mjs people "CTO AI startups"
|
||||
* ./exa-cli.mjs crawl "https://example.com"
|
||||
* ./exa-cli.mjs research "topic for deep research"
|
||||
* ./exa-cli.mjs research-check <taskId>
|
||||
*/
|
||||
|
||||
const MCP_BASE = 'https://mcp.exa.ai/mcp';
|
||||
const TOOLS_PARAM = 'tools=web_search_exa,web_search_advanced_exa,get_code_context_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check';
|
||||
|
||||
function getMcpUrl() {
|
||||
const apiKey = process.env.EXA_API_KEY;
|
||||
let url = `${MCP_BASE}?${TOOLS_PARAM}`;
|
||||
if (apiKey) {
|
||||
url += `&exaApiKey=${apiKey}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
async function callMcp(toolName, args) {
|
||||
const url = getMcpUrl();
|
||||
|
||||
const request = {
|
||||
jsonrpc: '2.0',
|
||||
id: Date.now(),
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: toolName,
|
||||
arguments: args
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json, text/event-stream',
|
||||
},
|
||||
body: JSON.stringify(request)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
|
||||
// Handle SSE streaming response
|
||||
if (contentType.includes('text/event-stream')) {
|
||||
const text = await response.text();
|
||||
const lines = text.split('\n');
|
||||
let result = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6);
|
||||
if (data && data !== '[DONE]') {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.result) {
|
||||
result = parsed.result;
|
||||
} else if (parsed.error) {
|
||||
throw new Error(`MCP Error: ${JSON.stringify(parsed.error)}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip non-JSON lines
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Handle regular JSON response
|
||||
const result = await response.json();
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(`MCP Error: ${JSON.stringify(result.error)}`);
|
||||
}
|
||||
|
||||
return result.result;
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const parsed = { _: [] };
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg.startsWith('--')) {
|
||||
const key = arg.slice(2);
|
||||
const next = args[i + 1];
|
||||
if (next && !next.startsWith('--')) {
|
||||
parsed[key] = next;
|
||||
i++;
|
||||
} else {
|
||||
parsed[key] = true;
|
||||
}
|
||||
} else {
|
||||
parsed._.push(arg);
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function formatContent(content) {
|
||||
if (!content) return '';
|
||||
if (Array.isArray(content)) {
|
||||
return content.map(c => c.text || JSON.stringify(c)).join('\n');
|
||||
}
|
||||
if (typeof content === 'string') return content;
|
||||
return JSON.stringify(content, null, 2);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const command = args._[0];
|
||||
const query = args._.slice(1).join(' ');
|
||||
const numResults = parseInt(args.num) || 10;
|
||||
|
||||
if (!command) {
|
||||
console.log(`Exa Search CLI
|
||||
|
||||
Commands:
|
||||
search <query> Web search
|
||||
search-advanced <query> Advanced search with filters
|
||||
code <query> Code/documentation search
|
||||
crawl <url> Get full page content
|
||||
company <name> Company research
|
||||
people <query> People/profile search
|
||||
research <topic> Start deep research
|
||||
research-check <taskId> Check research status
|
||||
|
||||
Options:
|
||||
--num <n> Number of results (default: 10)
|
||||
--domains <list> Comma-separated domains (advanced search)
|
||||
--after <date> Results after date YYYY-MM-DD (advanced search)
|
||||
--before <date> Results before date YYYY-MM-DD (advanced search)
|
||||
--tokens <n> Token limit for code search (default: 5000)
|
||||
|
||||
Environment:
|
||||
EXA_API_KEY Your Exa API key (optional, for higher rate limits)
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
switch (command) {
|
||||
case 'search':
|
||||
if (!query) throw new Error('Query required');
|
||||
result = await callMcp('web_search_exa', { query, numResults });
|
||||
break;
|
||||
|
||||
case 'search-advanced':
|
||||
if (!query) throw new Error('Query required');
|
||||
const advancedArgs = { query, numResults };
|
||||
if (args.domains) {
|
||||
advancedArgs.includeDomains = args.domains.split(',');
|
||||
}
|
||||
if (args.after) {
|
||||
advancedArgs.startPublishedDate = args.after;
|
||||
}
|
||||
if (args.before) {
|
||||
advancedArgs.endPublishedDate = args.before;
|
||||
}
|
||||
result = await callMcp('web_search_advanced_exa', advancedArgs);
|
||||
break;
|
||||
|
||||
case 'code':
|
||||
if (!query) throw new Error('Query required');
|
||||
const tokensNum = parseInt(args.tokens) || 5000;
|
||||
result = await callMcp('get_code_context_exa', { query, tokensNum });
|
||||
break;
|
||||
|
||||
case 'crawl':
|
||||
if (!query) throw new Error('URL required');
|
||||
result = await callMcp('crawling_exa', { url: query });
|
||||
break;
|
||||
|
||||
case 'company':
|
||||
if (!query) throw new Error('Company name required');
|
||||
result = await callMcp('company_research_exa', { companyName: query });
|
||||
break;
|
||||
|
||||
case 'people':
|
||||
if (!query) throw new Error('Query required');
|
||||
result = await callMcp('people_search_exa', { query, numResults });
|
||||
break;
|
||||
|
||||
case 'research':
|
||||
if (!query) throw new Error('Research topic required');
|
||||
result = await callMcp('deep_researcher_start', { query });
|
||||
break;
|
||||
|
||||
case 'research-check':
|
||||
if (!query) throw new Error('Task ID required');
|
||||
result = await callMcp('deep_researcher_check', { taskId: query });
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(formatContent(result?.content || result));
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user