Claude-code-plugins-plus-skills juicebox-sdk-patterns
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/juicebox-pack/skills/juicebox-sdk-patterns" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-juicebox-sdk-patterns && rm -rf "$T"
manifest:
plugins/saas-packs/juicebox-pack/skills/juicebox-sdk-patterns/SKILL.mdsource content
Juicebox SDK Patterns
Overview
Production-ready patterns for the Juicebox AI-powered people search API. Juicebox provides REST endpoints for searching professional profiles and enriching candidate data. The API authenticates via
JUICEBOX_API_KEY and returns structured profile objects with LinkedIn URLs as natural dedup keys. A singleton client centralizes rate-limit handling across search and enrich endpoints.
Singleton Client
const JUICEBOX_BASE = 'https://api.juicebox.work/v1'; let _client: JuiceboxClient | null = null; export function getClient(): JuiceboxClient { if (!_client) { const apiKey = process.env.JUICEBOX_API_KEY; if (!apiKey) throw new Error('JUICEBOX_API_KEY must be set — get it from juicebox.work/settings'); _client = new JuiceboxClient(apiKey); } return _client; } class JuiceboxClient { private headers: Record<string, string>; constructor(apiKey: string) { this.headers = { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }; } async search(query: string, limit = 20): Promise<SearchResponse> { const res = await fetch(`${JUICEBOX_BASE}/search`, { method: 'POST', headers: this.headers, body: JSON.stringify({ query, limit }) }); if (!res.ok) throw new JuiceboxError(res.status, await res.text()); return res.json(); } async enrich(linkedinUrl: string): Promise<Profile> { const res = await fetch(`${JUICEBOX_BASE}/enrich`, { method: 'POST', headers: this.headers, body: JSON.stringify({ linkedin_url: linkedinUrl }) }); if (!res.ok) throw new JuiceboxError(res.status, await res.text()); return res.json(); } }
Error Wrapper
export class JuiceboxError extends Error { constructor(public status: number, message: string) { super(message); this.name = 'JuiceboxError'; } } export async function safeCall<T>(operation: string, fn: () => Promise<T>): Promise<T> { try { return await fn(); } catch (err: any) { if (err instanceof JuiceboxError && err.status === 429) { await new Promise(r => setTimeout(r, 5000)); return fn(); } if (err instanceof JuiceboxError && err.status === 401) throw new JuiceboxError(401, 'Invalid JUICEBOX_API_KEY'); throw new JuiceboxError(err.status ?? 0, `${operation} failed: ${err.message}`); } }
Request Builder
class JuiceboxSearchBuilder { private body: Record<string, any> = {}; query(q: string) { this.body.query = q; return this; } limit(n: number) { this.body.limit = Math.min(n, 100); return this; } location(loc: string) { this.body.location = loc; return this; } title(t: string) { this.body.title_filter = t; return this; } company(c: string) { this.body.company_filter = c; return this; } yearsExp(min: number, max: number) { this.body.years_experience = { min, max }; return this; } build() { return this.body; } } // Usage: new JuiceboxSearchBuilder().query('ML engineer').location('San Francisco').yearsExp(3, 8).build();
Response Types
interface Profile { id: string; name: string; title: string; company: string; linkedin_url: string; location: string; skills: string[]; experience_years: number; } interface SearchResponse { profiles: Profile[]; total: number; has_more: boolean; cursor?: string; } interface EnrichResult { profile: Profile; education: Array<{ school: string; degree: string; year: number }>; experience: Array<{ company: string; title: string; start: string; end: string | null }>; }
Testing Utilities
export function mockProfile(overrides: Partial<Profile> = {}): Profile { return { id: 'prof-001', name: 'Jane Smith', title: 'Senior ML Engineer', company: 'Acme Corp', linkedin_url: 'https://linkedin.com/in/janesmith', location: 'San Francisco, CA', skills: ['Python', 'PyTorch', 'MLOps'], experience_years: 6, ...overrides }; } export function mockSearchResponse(count = 3): SearchResponse { return { profiles: Array.from({ length: count }, (_, i) => mockProfile({ id: `prof-${i}` })), total: count, has_more: false }; }
Error Handling
| Pattern | When to Use | Example |
|---|---|---|
wrapper | All Juicebox API calls | Structured error with operation context |
| Retry on 429 | Batch search pipelines | 5s backoff before retry |
| LinkedIn dedup | Multi-query search | on prevents duplicates |
| Cursor pagination | Search results > 100 | Pass from previous response |
Resources
Next Steps
Apply patterns in
juicebox-core-workflow-a.