/** * ClawdHub API client * * ClawdHub is a public skill registry at https://clawdhub.com * This client provides methods to fetch skills list and download skill packages. */ const CLAWDHUB_API_BASE = 'https://clawdhub.com/api/v1'; const CLAWDHUB_PAGE_LIMIT = 25; // Rate limiting: ClawdHub allows 120 requests/minute const RATE_LIMIT_DELAY_MS = 100; let lastRequestTime = 0; async function rateLimitedFetch(url, options = {}) { const maxAttempts = 10; let lastResponse = null; for (let attempt = 0; attempt < maxAttempts; attempt++) { const now = Date.now(); const elapsed = now - lastRequestTime; if (elapsed < RATE_LIMIT_DELAY_MS) { await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_DELAY_MS - elapsed)); } lastRequestTime = Date.now(); const response = await fetch(url, { ...options, headers: { Accept: 'application/json', 'User-Agent': 'OpenChamber/1.0', ...options.headers, }, }); lastResponse = response; if (response.status === 429 || response.status >= 500) { if (attempt < maxAttempts - 1) { const waitMs = 50 * (attempt + 1); await new Promise((resolve) => setTimeout(resolve, waitMs)); continue; } } return response; } return lastResponse; } /** * Fetch paginated list of skills from ClawdHub * @param {Object} options * @param {string} [options.cursor] - Pagination cursor from previous response * @returns {Promise<{ items: Array, nextCursor?: string }>} */ export async function fetchClawdHubSkills({ cursor } = {}) { const url = cursor ? `${CLAWDHUB_API_BASE}/skills?cursor=${encodeURIComponent(cursor)}&limit=${CLAWDHUB_PAGE_LIMIT}` : `${CLAWDHUB_API_BASE}/skills?limit=${CLAWDHUB_PAGE_LIMIT}`; const response = await rateLimitedFetch(url); if (!response.ok) { const text = await response.text().catch(() => ''); throw new Error(`ClawdHub API error (${response.status}): ${text || response.statusText}`); } const data = await response.json(); const nextCursor = (typeof data.nextCursor === 'string' && data.nextCursor) || (typeof data.next_cursor === 'string' && data.next_cursor) || (typeof data.next === 'string' && data.next) || (typeof data.cursor === 'string' && data.cursor) || null; return { items: data.items || [], nextCursor, }; } /** * Fetch details for a specific skill version * @param {string} slug - Skill slug/identifier * @param {string} [version='latest'] - Version string or 'latest' * @returns {Promise<{ skill: Object, version: Object }>} */ export async function fetchClawdHubSkillVersion(slug, version = 'latest') { // For 'latest', we need to first get the skill metadata to find the latest version if (version === 'latest') { const skillResponse = await rateLimitedFetch(`${CLAWDHUB_API_BASE}/skills/${encodeURIComponent(slug)}`); if (!skillResponse.ok) { throw new Error(`ClawdHub skill not found: ${slug}`); } const skillData = await skillResponse.json(); const latestVersion = skillData.skill?.tags?.latest || skillData.latestVersion?.version; if (!latestVersion) { throw new Error(`No latest version found for skill: ${slug}`); } version = latestVersion; } const url = `${CLAWDHUB_API_BASE}/skills/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}`; const response = await rateLimitedFetch(url); if (!response.ok) { const text = await response.text().catch(() => ''); throw new Error(`ClawdHub version error (${response.status}): ${text || response.statusText}`); } return response.json(); } /** * Download a skill package as a ZIP buffer * @param {string} slug - Skill slug/identifier * @param {string} version - Specific version string * @returns {Promise} - ZIP file contents */ export async function downloadClawdHubSkill(slug, version) { const versionParam = typeof version === 'string' && version !== 'latest' ? `&version=${encodeURIComponent(version)}` : '&tag=latest'; const url = `${CLAWDHUB_API_BASE}/download?slug=${encodeURIComponent(slug)}${versionParam}`; const response = await rateLimitedFetch(url, { headers: { Accept: 'application/zip', }, }); if (!response.ok) { const text = await response.text().catch(() => ''); throw new Error(`ClawdHub download error (${response.status}): ${text || response.statusText}`); } return response.arrayBuffer(); } /** * Get skill metadata without version details * @param {string} slug - Skill slug/identifier * @returns {Promise} */ export async function fetchClawdHubSkillInfo(slug) { const url = `${CLAWDHUB_API_BASE}/skills/${encodeURIComponent(slug)}`; const response = await rateLimitedFetch(url); if (!response.ok) { const text = await response.text().catch(() => ''); throw new Error(`ClawdHub skill error (${response.status}): ${text || response.statusText}`); } return response.json(); }