import { Type } from "typebox"; import { HttpError, HttpResponseLike, HttpStatusError } from "../shared/http"; import { isFetchEnabled } from "../shared/env"; import { chunkAndSerializeResponse, serializeResponseWithMeta } from "../shared/serialize"; import { chunkingManager } from "../shared/instances"; import type { HttpClientFactory, Provider, ProviderMetadata, ProviderResult, ToolDefinition } from "./base"; import { BaseProvider } from "./base"; function safeJsonLoads(text: string): any { return JSON.parse(text); } function pythonString(value: unknown): string { return value === null || value === undefined ? "None" : String(value); } function buildRepoPath(image: string): string { return image.includes("/") ? image : `library/${image}`; } function buildDisplayName(repoOwner: unknown, repoName: unknown): string { const owner = repoOwner ?? ""; const name = repoName ?? ""; return owner ? `${owner}/${name}` : `library/${name}`; } export class DockerHubProvider extends BaseProvider implements Provider { static readonly DOCKERHUB_API_URL = "https://hub.docker.com/v2"; constructor(httpClientFactory: HttpClientFactory) { super(httpClientFactory); } getMetadata(): ProviderMetadata { const toolNames = ["search_docker_images", "docker_image_metadata"]; if (isFetchEnabled()) { toolNames.push("fetch_docker_image_docs"); toolNames.push("fetch_dockerfile"); } return { name: "dockerhub", description: "DockerHub Docker image search and metadata", exposeAsTool: true, toolNames, supportsLibrarySearch: false, requiredEnvVars: [], optionalEnvVars: [], toolTiers: { search_docker_images: { tier: 2, deferRecommended: true, category: "search" }, docker_image_metadata: { tier: 3, deferRecommended: true, category: "metadata" }, fetch_docker_image_docs: { tier: 3, deferRecommended: true, category: "fetch" }, fetch_dockerfile: { tier: 4, deferRecommended: true, category: "fetch" }, }, }; } async searchLibrary(_library: string, _limit = 5): Promise { return { success: false, error: null, providerName: "dockerhub" }; } private async _fetchRepositoryData(image: string): Promise> { const repoPath = buildRepoPath(image); const url = `${DockerHubProvider.DOCKERHUB_API_URL}/repositories/${repoPath}/`; const client = await this.httpClient(); const response = (await client.get(url)) as HttpResponseLike; response.raiseForStatus(); return safeJsonLoads(response.text) as Record; } async _searchImages(query: string, limit = 5): Promise> { try { const url = `${DockerHubProvider.DOCKERHUB_API_URL}/search/repositories/`; const client = await this.httpClient(); const response = (await client.get(url, { params: { query, page_size: limit } })) as HttpResponseLike; response.raiseForStatus(); const payload = safeJsonLoads(response.text) as Record; const results: Record[] = []; for (const rawItem of (Array.isArray(payload.results) ? payload.results : []) as Record[]) { const repoName = rawItem.repo_name ?? ""; const repoOwner = rawItem.repo_owner ?? ""; const displayName = buildDisplayName(repoOwner, repoName); results.push({ name: repoName, owner: repoOwner || "library", description: rawItem.short_description ?? "", star_count: rawItem.star_count ?? 0, pull_count: rawItem.pull_count ?? 0, is_official: rawItem.is_official ?? false, url: `https://hub.docker.com/r/${displayName}`, }); } return { query, count: results.length, results, }; } catch (error) { if (error instanceof HttpStatusError) { return { query, error: `DockerHub returned ${error.response.status}`, results: [], }; } if (error instanceof HttpError) { return { query, error: `DockerHub request failed: ${error instanceof Error ? error.message : String(error)}`, results: [], }; } return { query, error: `Failed to search images: ${error instanceof Error ? error.message : String(error)}`, results: [], }; } } async _fetchImageMetadata(image: string): Promise> { try { const data = await this._fetchRepositoryData(image); const namespace = data.namespace ?? null; const name = data.name ?? null; return { name, namespace, full_name: data.full_name ?? `${pythonString(namespace)}/${pythonString(name)}`, description: data.description ?? "", readme: data.readme ?? "", last_updated: data.last_updated, star_count: data.star_count ?? 0, pull_count: data.pull_count ?? 0, is_official: data.is_official ?? false, is_private: data.is_private ?? false, repository_type: data.repository_type, url: `https://hub.docker.com/r/${buildRepoPath(image)}`, }; } catch (error) { if (error instanceof HttpStatusError) { if (error.response.status === 404) { return { image, error: "Image not found on DockerHub", }; } return { image, error: `DockerHub returned ${error.response.status}`, }; } if (error instanceof HttpError) { return { image, error: `DockerHub request failed: ${error instanceof Error ? error.message : String(error)}`, }; } return { image, error: `Failed to fetch metadata: ${error instanceof Error ? error.message : String(error)}`, }; } } async _fetchImageDocs(image: string, maxBytes = 20480): Promise> { try { const metadata = await this._fetchImageMetadata(image); if ("error" in metadata) { return { image, content: "", error: metadata.error, size_bytes: 0, source: null, }; } const description = typeof metadata.description === "string" ? metadata.description : ""; const readme = typeof metadata.readme === "string" ? metadata.readme : ""; let content = ""; if (description) { content = `## Description\n\n${description}\n\n`; } if (readme) { content += `## README\n\n${readme}`; } else if (!description) { content = "No documentation available for this image."; } const contentBytes = Buffer.byteLength(content, "utf8"); const truncated = contentBytes > maxBytes; if (truncated) { content = content.slice(0, maxBytes); } return { image, content, size_bytes: Buffer.byteLength(content, "utf8"), source: "dockerhub", truncated, }; } catch (error) { return { image, content: "", error: `Failed to fetch docs: ${error instanceof Error ? error.message : String(error)}`, size_bytes: 0, source: null, }; } } async _fetchDockerfile(image: string): Promise> { try { const metadata = await this._fetchImageMetadata(image); if ("error" in metadata) { return { image, error: metadata.error, source: null, }; } const data = await this._fetchRepositoryData(image); const fullDescription = typeof data.full_description === "string" ? data.full_description : ""; const githubPattern = /https:\/\/github\.com\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+\/blob\/[a-zA-Z0-9_.-]+(?:\/[a-zA-Z0-9_.-]+)*\/Dockerfile/g; const matches = fullDescription.match(githubPattern) ?? []; if (!matches.length) { return { image, error: "No GitHub Dockerfile link found in image description", source: "dockerhub_description", }; } const dockerfileUrl = matches[0]!; const rawUrl = dockerfileUrl.replace("github.com", "raw.githubusercontent.com").replace("/blob/", "/"); const client = await this.httpClient(); const response = (await client.get(rawUrl)) as HttpResponseLike; response.raiseForStatus(); const content = response.text; return { image, content, size_bytes: Buffer.byteLength(content, "utf8"), source: rawUrl, found_in_description: true, }; } catch (error) { return { image, error: `Failed to fetch Dockerfile: ${error instanceof Error ? error.message : String(error)}`, source: null, }; } } getTools(): Record { const searchDockerImages: ToolDefinition = { name: "search_docker_images", label: "Search Docker images", description: "Search DockerHub for images by name or keywords. Returns names, descriptions, stars, pulls, and official status.", promptSnippet: 'search_docker_images("nginx")', parameters: Type.Object({ query: Type.String({ description: "Search query" }), limit: Type.Optional(Type.Integer({ description: "Maximum number of results" })), }), execute: async (_id: string, { query, limit = 5 }: { query: string; limit?: number }) => { const result = await this._searchImages(query, limit); return serializeResponseWithMeta(result); }, }; const dockerImageMetadata: ToolDefinition = { name: "docker_image_metadata", label: "Docker image metadata", description: "Get Docker image metadata such as stats, description, official status, and repository details.", promptSnippet: 'docker_image_metadata("nginx")', parameters: Type.Object({ image: Type.String({ description: "Image name" }), }), execute: async (_id: string, { image }: { image: string }) => { const result = await this._fetchImageMetadata(image); return serializeResponseWithMeta(result); }, }; const tools: Record = { search_docker_images: searchDockerImages, docker_image_metadata: dockerImageMetadata, }; if (isFetchEnabled()) { tools.fetch_docker_image_docs = { name: "fetch_docker_image_docs", label: "Fetch Docker image docs", description: "Fetch DockerHub README documentation for an image.", promptSnippet: 'fetch_docker_image_docs("nginx")', parameters: Type.Object({ image: Type.String({ description: "Image name" }), max_bytes: Type.Optional(Type.Integer({ description: "Maximum bytes" })), }), execute: async (_id: string, { image, max_bytes = 20480 }: { image: string; max_bytes?: number }) => { const result = await this._fetchImageDocs(image, max_bytes); return chunkAndSerializeResponse(result as Record, "content", chunkingManager); }, }; tools.fetch_dockerfile = { name: "fetch_dockerfile", label: "Fetch Dockerfile", description: "Fetch the Dockerfile used to build an image when linked from its description.", promptSnippet: 'fetch_dockerfile("nginx")', parameters: Type.Object({ image: Type.String({ description: "Image name" }), }), execute: async (_id: string, { image }: { image: string }) => { const result = await this._fetchDockerfile(image); return chunkAndSerializeResponse(result as Record, "content", chunkingManager); }, }; } return tools; } } export { DockerHubProvider as DockerhubProvider };