openclaw/src/clawdhub/client.ts
ronitchidara 676e3a1254 fix: resolve lint errors from transformation phases
- Remove unnecessary escape character in exec-blocklist regex
- Fix header spread in clawdhub client to handle all HeadersInit types
- Remove unused InjectionSeverity import from chat-sanitize test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 17:01:41 +05:30

221 lines
5.2 KiB
TypeScript

/**
* ClawdHub API client for the public skills registry.
* Handles search, details, install, and update operations.
*/
const DEFAULT_REGISTRY_URL = "https://clawdhub.com";
export type ClawdHubSearchResult = {
slug: string;
name: string;
description: string;
emoji?: string;
author?: string;
version: string;
downloads: number;
stars: number;
updatedAt: string;
tags: string[];
};
export type ClawdHubSearchResponse = {
results: ClawdHubSearchResult[];
total: number;
query: string;
};
export type ClawdHubSkillVersion = {
version: string;
changelog?: string;
publishedAt: string;
tags: string[];
downloads: number;
};
export type ClawdHubSkillDetails = {
slug: string;
name: string;
description: string;
emoji?: string;
readme?: string;
author?: string;
homepage?: string;
repository?: string;
license?: string;
currentVersion: string;
versions: ClawdHubSkillVersion[];
downloads: number;
stars: number;
createdAt: string;
updatedAt: string;
tags: string[];
};
export type ClawdHubInstalledSkill = {
slug: string;
version: string;
installedAt: string;
path: string;
};
export type ClawdHubLockFile = {
version: number;
skills: Record<string, ClawdHubInstalledSkill>;
};
export type ClawdHubInstallResult = {
ok: boolean;
slug: string;
version: string;
path: string;
message?: string;
};
export type ClawdHubUpdateCheck = {
slug: string;
currentVersion: string;
latestVersion: string;
hasUpdate: boolean;
};
export type ClawdHubClientOptions = {
registryUrl?: string;
timeout?: number;
};
function getRegistryUrl(): string {
return process.env.CLAWDHUB_REGISTRY || DEFAULT_REGISTRY_URL;
}
async function fetchJson<T>(url: string, options: RequestInit = {}, timeoutMs = 30000): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const headers: Record<string, string> = {
Accept: "application/json",
"User-Agent": "clawdbot-gateway",
};
if (options.headers) {
const h = options.headers;
if (h instanceof Headers) {
h.forEach((value, key) => {
headers[key] = value;
});
} else if (Array.isArray(h)) {
for (const [key, value] of h) {
headers[key] = value;
}
} else {
Object.assign(headers, h);
}
}
const response = await fetch(url, {
...options,
signal: controller.signal,
headers,
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
}
return (await response.json()) as T;
} finally {
clearTimeout(timeoutId);
}
}
/**
* Search for skills in ClawdHub using vector search.
*/
export async function searchSkills(
query: string,
options: ClawdHubClientOptions = {},
): Promise<ClawdHubSearchResponse> {
const registryUrl = options.registryUrl || getRegistryUrl();
const params = new URLSearchParams({ q: query });
const url = `${registryUrl}/api/skills/search?${params}`;
return fetchJson<ClawdHubSearchResponse>(url, {}, options.timeout);
}
/**
* Get detailed information about a skill including all versions.
*/
export async function getSkillDetails(
slug: string,
options: ClawdHubClientOptions = {},
): Promise<ClawdHubSkillDetails> {
const registryUrl = options.registryUrl || getRegistryUrl();
const url = `${registryUrl}/api/skills/${encodeURIComponent(slug)}`;
return fetchJson<ClawdHubSkillDetails>(url, {}, options.timeout);
}
/**
* Download a skill zip from ClawdHub.
*/
export async function downloadSkillZip(
slug: string,
version: string,
options: ClawdHubClientOptions = {},
): Promise<ArrayBuffer> {
const registryUrl = options.registryUrl || getRegistryUrl();
const url = `${registryUrl}/api/skills/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}/download`;
const controller = new AbortController();
const timeoutMs = options.timeout || 60000;
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
signal: controller.signal,
headers: {
"User-Agent": "clawdbot-gateway",
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.arrayBuffer();
} finally {
clearTimeout(timeoutId);
}
}
/**
* Check for updates across multiple installed skills.
*/
export async function checkUpdates(
installed: ClawdHubInstalledSkill[],
options: ClawdHubClientOptions = {},
): Promise<ClawdHubUpdateCheck[]> {
const results: ClawdHubUpdateCheck[] = [];
for (const skill of installed) {
try {
const details = await getSkillDetails(skill.slug, options);
results.push({
slug: skill.slug,
currentVersion: skill.version,
latestVersion: details.currentVersion,
hasUpdate: details.currentVersion !== skill.version,
});
} catch {
// Skip skills that can't be fetched
results.push({
slug: skill.slug,
currentVersion: skill.version,
latestVersion: skill.version,
hasUpdate: false,
});
}
}
return results;
}