Files
openchamber/packages/web/server/lib/skills-catalog/clawdhub/scan.js
T
Bohdan Triapitsyn 25525d5b1b feat: enhance ClawdHub integration with paging and retry
Add paging support when loading ClawdHub skills
Retry on API errors and throttle for ClawdHub requests
Load source content on source change and show loading indicators
2026-01-26 19:50:11 +02:00

114 lines
3.2 KiB
JavaScript

/**
* ClawdHub skill scanning
*
* Fetches all available skills from the ClawdHub registry
* and transforms them into SkillsCatalogItem format.
*/
import { fetchClawdHubSkills } from './api.js';
const MAX_PAGES = 20; // Safety limit to prevent infinite loops
const CLAWDHUB_PAGE_LIMIT = 25;
const mapClawdHubItem = (item) => {
const latestVersion = item.tags?.latest || item.latestVersion?.version || '1.0.0';
return {
sourceId: 'clawdhub',
repoSource: 'clawdhub:registry',
repoSubpath: null,
gitIdentityId: null,
skillDir: item.slug,
skillName: item.slug,
frontmatterName: item.displayName || item.slug,
description: item.summary || null,
installable: true,
warnings: [],
// ClawdHub-specific metadata
clawdhub: {
slug: item.slug,
version: latestVersion,
displayName: item.displayName,
owner: item.owner?.handle || null,
downloads: item.stats?.downloads || 0,
stars: item.stats?.stars || 0,
versionsCount: item.stats?.versions || 1,
createdAt: item.createdAt,
updatedAt: item.updatedAt,
},
};
};
/**
* Scan ClawdHub registry for all available skills
* @returns {Promise<{ ok: boolean, items?: Array, error?: Object }>}
*/
export async function scanClawdHub() {
try {
const allItems = [];
let cursor = null;
for (let page = 0; page < MAX_PAGES; page++) {
let items = [];
let nextCursor = null;
try {
const pageResult = await fetchClawdHubSkills({ cursor });
items = pageResult.items || [];
nextCursor = pageResult.nextCursor || null;
} catch (error) {
if (page > 0 && allItems.length > 0) {
console.warn('ClawdHub pagination failed; returning partial results.');
break;
}
throw error;
}
for (const item of items) {
allItems.push(mapClawdHubItem(item));
}
if (!nextCursor) {
break;
}
cursor = nextCursor;
}
// Sort by downloads (most popular first)
allItems.sort((a, b) => (b.clawdhub?.downloads || 0) - (a.clawdhub?.downloads || 0));
return { ok: true, items: allItems };
} catch (error) {
console.error('ClawdHub scan error:', error);
return {
ok: false,
error: {
kind: 'networkError',
message: error instanceof Error ? error.message : 'Failed to fetch skills from ClawdHub',
},
};
}
}
/**
* Scan a single ClawdHub page (cursor-based)
* @returns {Promise<{ ok: boolean, items?: Array, nextCursor?: string | null, error?: Object }>}
*/
export async function scanClawdHubPage({ cursor } = {}) {
try {
const { items, nextCursor } = await fetchClawdHubSkills({ cursor });
const mapped = (items || []).map(mapClawdHubItem).slice(0, CLAWDHUB_PAGE_LIMIT);
mapped.sort((a, b) => (b.clawdhub?.downloads || 0) - (a.clawdhub?.downloads || 0));
return { ok: true, items: mapped, nextCursor: nextCursor || null };
} catch (error) {
console.error('ClawdHub page scan error:', error);
return {
ok: false,
error: {
kind: 'networkError',
message: error instanceof Error ? error.message : 'Failed to fetch skills from ClawdHub',
},
};
}
}