UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

94 lines (93 loc) • 4.63 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. import { abortableDelay, backoffDelay, computeRetryDelay, jsonWithByteCap, ResponseTooLargeError, shouldRetry, toErrorMessage, } from "../core/common.js"; import { ConfigError, NotFoundError, TransientError } from "../core/errors.js"; import { formatRegistryUrl } from "../core/registry-url.js"; const DEFAULT_TIMEOUT_MS = 30_000; const DEFAULT_RETRIES = 2; const DEFAULT_JSON_BYTE_CAP = 10 * 1024 * 1024; const MAX_RETRY_DELAY_MS = 30_000; /** Fetch a registry URL. Resolves only to a 2xx response; every other outcome throws an `AkmError`. */ export async function fetchRegistry(rawUrl, options = {}) { const url = parseRegistryUrl(rawUrl); const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; const retries = options.retries ?? DEFAULT_RETRIES; for (let attempt = 0;; attempt += 1) { // AbortSignal.timeout() does not keep the event loop alive, and it keeps // covering the body after this function returns. const timeout = AbortSignal.timeout(timeoutMs); const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout; let response; try { response = await fetch(rawUrl, { headers: options.headers, signal, redirect: "follow" }); } catch (error) { if (options.signal?.aborted) throw error; if (attempt < retries) { await abortableDelay(backoffDelay(attempt, undefined, MAX_RETRY_DELAY_MS), options.signal); continue; } const reason = timeout.aborted ? `timed out after ${timeoutMs}ms` : `failed: ${toErrorMessage(error)}`; throw new TransientError(`Registry request to ${url.host} ${reason} (${rawUrl})`, "REGISTRY_UNREACHABLE"); } if (attempt < retries && shouldRetry(response.status)) { await cancelBody(response); await abortableDelay(computeRetryDelay(response, attempt, { maxDelayMs: MAX_RETRY_DELAY_MS }), options.signal); continue; } if (response.ok) return response; await cancelBody(response); throw statusError(response.status, rawUrl); } } /** `fetchRegistry` plus a JSON body read under a byte cap. */ export async function fetchRegistryJson(rawUrl, options = {}) { const response = await fetchRegistry(rawUrl, options); try { return await jsonWithByteCap(response, options.maxBytes ?? DEFAULT_JSON_BYTE_CAP, { bodyTimeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS, signal: options.signal, }); } catch (error) { if (options.signal?.aborted) throw error; if (error instanceof ResponseTooLargeError || error instanceof SyntaxError) { throw new NotFoundError(`Registry response from ${rawUrl} is not usable: ${toErrorMessage(error)}`, "REGISTRY_RESPONSE_INVALID"); } throw new TransientError(`Registry response from ${rawUrl} could not be read: ${toErrorMessage(error)}`, "REGISTRY_UNREACHABLE"); } } function parseRegistryUrl(rawUrl) { let url; try { url = new URL(rawUrl); } catch { throw new ConfigError(`Registry URL is not a valid URL: ${formatRegistryUrl(rawUrl)}`, "REGISTRY_URL_INVALID"); } if (url.protocol !== "http:" && url.protocol !== "https:") { throw new ConfigError(`Registry URL must use http(s), not ${url.protocol || "an empty scheme"}: ${formatRegistryUrl(rawUrl)}`, "REGISTRY_URL_INVALID"); } if (url.username || url.password) { throw new ConfigError(`Registry URL must not embed credentials: ${url.origin}`, "REGISTRY_URL_INVALID"); } return url; } function statusError(status, rawUrl) { const message = `Registry request failed (HTTP ${status}) for ${rawUrl}`; if (shouldRetry(status)) return new TransientError(message, "REGISTRY_UNREACHABLE"); if (status === 404 || status === 410) return new NotFoundError(message, "REGISTRY_NOT_FOUND"); const hint = status === 401 || status === 403 ? "The registry refused the request. For GitHub, set GITHUB_TOKEN or sign in with `gh auth login`; akm's registry providers send no other credentials." : undefined; return new NotFoundError(message, "REGISTRY_RESPONSE_INVALID", hint); } async function cancelBody(response) { await response.body?.cancel().catch(() => undefined); }