UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

948 lines (947 loc) 31.5 kB
import { t as sanitizeForLog } from "./ansi-DrXAcdMD.js"; import { l as toErrorObject } from "./error-coercion-D_-xJ90S.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { a as resolveOsHomeRelativePath, c as resolveUserPath } from "./home-dir-BPhrG-aM.js"; import { d as pathExists } from "./fs-safe-B6pvPGnf.js"; import { r as isPathInside } from "./path-guards-Cp-mGr3-.js"; import "./utils-P__uGsPB.js"; import { o as redactSensitiveUrlLikeString } from "./redact-sensitive-url-BN1NZvXG.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { r as readRegularFile } from "./regular-file-Equ4rrfE.js"; import { p as tryReadJson } from "./json-files-Bq1lIlQB.js"; import { t as hasHttpUrlPrefix } from "./url-protocol-OU3K-ySz.js"; import { r as runCommandWithTimeout } from "./exec-BIE-3oLG.js"; import { i as fetchWithSsrFGuard } from "./fetch-guard-BMdGQhbb.js"; import { h as resolveArchiveKind } from "./archive-BfjFIxAJ.js"; import { z as copyPluginInstallTransactionRequest } from "./install-managed-npm-state-BlNS_UI7.js"; import { n as installPluginFromPath } from "./install-BezM8CtS.js"; import { n as isImmutableGitCommitRef } from "./git-install-BNE75LQq.js"; import path from "node:path"; import fs from "node:fs/promises"; import os from "node:os"; //#region src/plugins/marketplace.ts const DEFAULT_GIT_TIMEOUT_MS = 12e4; const DEFAULT_MARKETPLACE_DOWNLOAD_TIMEOUT_MS = 12e4; const MAX_MARKETPLACE_ARCHIVE_BYTES = 268435456; const MAX_MARKETPLACE_MANIFEST_BYTES = 16777216; const MARKETPLACE_MANIFEST_CANDIDATES = [path.join(".claude-plugin", "marketplace.json"), "marketplace.json"]; const CLAUDE_KNOWN_MARKETPLACES_PATH = path.join("~", ".claude", "plugins", "known_marketplaces.json"); function isGitUrl(value) { return /^git@/i.test(value) || /^ssh:\/\//i.test(value) || /^https?:\/\/.+\.git(?:#.*)?$/i.test(value); } function looksLikeGitHubRepoShorthand(value) { return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#.+)?$/.test(value.trim()); } function splitRef(value) { const trimmed = value.trim(); const hashIndex = trimmed.lastIndexOf("#"); if (hashIndex <= 0 || hashIndex >= trimmed.length - 1) return { base: trimmed }; return { base: trimmed.slice(0, hashIndex), ref: normalizeOptionalString(trimmed.slice(hashIndex + 1)) }; } function normalizeEntrySource(raw) { if (typeof raw === "string") { const trimmed = raw.trim(); if (!trimmed) return { ok: false, error: "empty plugin source" }; if (hasHttpUrlPrefix(trimmed)) return { ok: true, source: { kind: "url", url: trimmed } }; return { ok: true, source: { kind: "path", path: trimmed } }; } if (!raw || typeof raw !== "object") return { ok: false, error: "plugin source must be a string or object" }; const rec = raw; const kind = normalizeOptionalString(rec.type) ?? normalizeOptionalString(rec.source); if (!kind) return { ok: false, error: "plugin source object missing \"type\" or \"source\"" }; if (kind === "path") { const sourcePath = normalizeOptionalString(rec.path); if (!sourcePath) return { ok: false, error: "path source missing \"path\"" }; return { ok: true, source: { kind: "path", path: sourcePath } }; } if (kind === "github") { const repo = normalizeOptionalString(rec.repo) ?? normalizeOptionalString(rec.url); if (!repo) return { ok: false, error: "github source missing \"repo\"" }; return { ok: true, source: { kind: "github", repo, path: normalizeOptionalString(rec.path), ref: normalizeOptionalString(rec.ref) ?? normalizeOptionalString(rec.branch) ?? normalizeOptionalString(rec.tag) } }; } if (kind === "git") { const url = normalizeOptionalString(rec.url) ?? normalizeOptionalString(rec.repo); if (!url) return { ok: false, error: "git source missing \"url\"" }; return { ok: true, source: { kind: "git", url, path: normalizeOptionalString(rec.path), ref: normalizeOptionalString(rec.ref) ?? normalizeOptionalString(rec.branch) ?? normalizeOptionalString(rec.tag) } }; } if (kind === "git-subdir") { const url = normalizeOptionalString(rec.url) ?? normalizeOptionalString(rec.repo); const sourcePath = normalizeOptionalString(rec.path) ?? normalizeOptionalString(rec.subdir); if (!url) return { ok: false, error: "git-subdir source missing \"url\"" }; if (!sourcePath) return { ok: false, error: "git-subdir source missing \"path\"" }; return { ok: true, source: { kind: "git-subdir", url, path: sourcePath, ref: normalizeOptionalString(rec.ref) ?? normalizeOptionalString(rec.branch) ?? normalizeOptionalString(rec.tag) } }; } if (kind === "url") { const url = normalizeOptionalString(rec.url); if (!url) return { ok: false, error: "url source missing \"url\"" }; return { ok: true, source: { kind: "url", url } }; } return { ok: false, error: `unsupported plugin source kind: ${kind}` }; } function marketplaceEntrySourceToInput(source) { switch (source.kind) { case "path": return source.path; case "github": return `${source.repo}${source.ref ? `#${source.ref}` : ""}`; case "git": return `${source.url}${source.ref ? `#${source.ref}` : ""}`; case "git-subdir": return `${source.url}${source.ref ? `#${source.ref}` : ""}`; case "url": return source.url; } throw new Error("Unsupported marketplace entry source"); } function marketplaceEntryGitRef(source) { switch (source.kind) { case "github": case "git": case "git-subdir": return source.ref; case "url": return resolveArchiveKind(source.url) ? void 0 : normalizeGitCloneSource(source.url)?.ref; case "path": return; } throw new Error("Unsupported marketplace entry source"); } function isMutableGitDerivedSource(ref) { return !isImmutableGitCommitRef(ref); } function marketplaceInstallPolicySource(params) { const marketplaceMutable = isMutableGitDerivedSource(params.marketplaceRef); const entryMutable = isMutableGitDerivedSource(marketplaceEntryGitRef(params.source)); if (resolveArchiveKind(params.resolvedPath)) { if (params.marketplaceOrigin === "remote" && params.source.kind === "path" && !hasHttpUrlPrefix(params.source.path)) return { kind: "archive", authority: "third-party", mutable: marketplaceMutable, network: true }; if (params.source.kind === "path" && !hasHttpUrlPrefix(params.source.path)) return { kind: "archive", authority: "user", mutable: true, network: false }; return { kind: "archive", authority: "third-party", mutable: entryMutable, network: true }; } if (params.marketplaceOrigin === "remote" && params.source.kind === "path" && !hasHttpUrlPrefix(params.source.path)) return { kind: "git", authority: "third-party", mutable: marketplaceMutable, network: true }; if (params.source.kind === "path") { if (hasHttpUrlPrefix(params.source.path)) return { kind: "archive", authority: "third-party", mutable: true, network: true }; return { kind: "local-path", authority: "user", mutable: true, network: false }; } if (params.source.kind === "url") return { kind: resolveArchiveKind(params.source.url) ? "archive" : "git", authority: "third-party", mutable: entryMutable, network: true }; return { kind: "git", authority: "third-party", mutable: entryMutable, network: true }; } function marketplaceInstallPolicyRequestKind(params) { if (resolveArchiveKind(params.resolvedPath)) return "plugin-archive"; if (params.marketplaceOrigin === "remote") return "plugin-git"; if (params.source.kind === "github" || params.source.kind === "git" || params.source.kind === "git-subdir") return "plugin-git"; if (params.source.kind === "url" && !resolveArchiveKind(params.source.url)) return "plugin-git"; return "plugin-dir"; } function parseMarketplaceManifest(raw, sourceLabel) { let parsed; try { parsed = JSON.parse(raw); } catch (err) { return { ok: false, error: `invalid marketplace JSON at ${sourceLabel}: ${String(err)}` }; } if (!parsed || typeof parsed !== "object") return { ok: false, error: `invalid marketplace JSON at ${sourceLabel}: expected object` }; const rec = parsed; if (!Array.isArray(rec.plugins)) return { ok: false, error: `invalid marketplace JSON at ${sourceLabel}: missing plugins[]` }; const plugins = []; for (const entry of rec.plugins) { if (!entry || typeof entry !== "object") return { ok: false, error: `invalid marketplace entry in ${sourceLabel}: expected object` }; const plugin = entry; const name = normalizeOptionalString(plugin.name); if (!name) return { ok: false, error: `invalid marketplace entry in ${sourceLabel}: missing name` }; const normalizedSource = normalizeEntrySource(plugin.source); if (!normalizedSource.ok) return { ok: false, error: `invalid marketplace entry "${name}" in ${sourceLabel}: ${normalizedSource.error}` }; plugins.push({ name, version: normalizeOptionalString(plugin.version), description: normalizeOptionalString(plugin.description), source: normalizedSource.source }); } return { ok: true, manifest: { name: normalizeOptionalString(rec.name), version: normalizeOptionalString(rec.version), plugins } }; } async function readClaudeKnownMarketplaces() { const knownPath = resolveOsHomeRelativePath(CLAUDE_KNOWN_MARKETPLACES_PATH); if (!await pathExists(knownPath)) return {}; const parsed = await tryReadJson(knownPath); if (!parsed || typeof parsed !== "object") return {}; const entries = parsed; const result = {}; for (const [name, value] of Object.entries(entries)) { if (!value || typeof value !== "object") continue; const record = value; result[name] = { installLocation: normalizeOptionalString(record.installLocation), source: record.source }; } return result; } function deriveMarketplaceRootFromManifestPath(manifestPath) { const manifestDir = path.dirname(manifestPath); return path.basename(manifestDir) === ".claude-plugin" ? path.dirname(manifestDir) : manifestDir; } async function resolveLocalMarketplaceSource(input) { const resolved = resolveUserPath(input); if (!await pathExists(resolved)) return null; const stat = await fs.stat(resolved); if (stat.isFile()) return { ok: true, rootDir: deriveMarketplaceRootFromManifestPath(resolved), manifestPath: resolved }; if (!stat.isDirectory()) return { ok: false, error: `unsupported marketplace source: ${resolved}` }; const rootDir = path.basename(resolved) === ".claude-plugin" ? path.dirname(resolved) : resolved; for (const candidate of MARKETPLACE_MANIFEST_CANDIDATES) { const manifestPath = path.join(rootDir, candidate); if (await pathExists(manifestPath)) return { ok: true, rootDir, manifestPath }; } return { ok: false, error: `marketplace manifest not found under ${resolved}` }; } function normalizeGitCloneSource(source) { const split = splitRef(source); if (looksLikeGitHubRepoShorthand(split.base)) return { url: `https://github.com/${split.base}.git`, ref: split.ref, label: split.base }; if (isGitUrl(source)) return { url: split.base, ref: split.ref, label: split.base }; if (hasHttpUrlPrefix(source)) try { const url = new URL(split.base); if (url.hostname !== "github.com") return null; const parts = url.pathname.replace(/\/+$/, "").split("/").filter(Boolean); if (parts.length < 2) return null; const repo = `${parts[0]}/${parts[1]?.replace(/\.git$/i, "")}`; return { url: `https://github.com/${repo}.git`, ref: split.ref, label: repo }; } catch { return null; } return null; } async function cloneMarketplaceRepo(params) { const normalized = normalizeGitCloneSource(params.source); if (!normalized) return { ok: false, error: `unsupported marketplace source: ${params.source}` }; const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-marketplace-")); const repoDir = path.join(tmpDir, "repo"); const refIsCommit = isImmutableGitCommitRef(normalized.ref); const argv = ["git", "clone"]; if (!normalized.ref) argv.push("--depth", "1"); else if (!refIsCommit) { argv.push("--depth", "1"); argv.push("--branch", normalized.ref); } argv.push(normalized.url, repoDir); params.logger?.info?.(`Cloning marketplace source ${normalized.label}...`); const res = await runCommandWithTimeout(argv, { timeoutMs: params.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS }); if (res.code !== 0) { await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => void 0); const detail = res.stderr.trim() || res.stdout.trim() || "git clone failed"; return { ok: false, error: `failed to clone marketplace source ${normalized.label}: ${detail}` }; } if (refIsCommit) { const checkout = await runCommandWithTimeout([ "git", "switch", "--detach", "--", normalized.ref ], { cwd: repoDir, timeoutMs: params.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS }); if (checkout.code !== 0) { await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => void 0); const detail = checkout.stderr.trim() || checkout.stdout.trim() || "git checkout failed"; return { ok: false, error: `failed to checkout marketplace source ${normalized.label}: ${detail}` }; } } return { ok: true, rootDir: repoDir, label: normalized.label, ...normalized.ref ? { ref: normalized.ref } : {}, cleanup: async () => { await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => void 0); } }; } async function loadMarketplace(params) { const loadMarketplaceFromManifestFile = async (paramsLocal) => { let raw; try { const resolvedManifestPath = await fs.realpath(paramsLocal.manifestPath); const { buffer } = await readRegularFile({ filePath: resolvedManifestPath, maxBytes: MAX_MARKETPLACE_MANIFEST_BYTES }); raw = buffer.toString("utf-8"); } catch (err) { await paramsLocal.cleanup?.(); const message = err instanceof Error ? err.message : String(err); if (message.startsWith("File exceeds")) return { ok: false, error: "Marketplace manifest too large" }; return { ok: false, error: `Marketplace manifest unreadable: ${message}` }; } const parsed = parseMarketplaceManifest(raw, paramsLocal.manifestPath); if (!parsed.ok) { await paramsLocal.cleanup?.(); return parsed; } const validated = await validateMarketplaceManifest({ manifest: parsed.manifest, sourceLabel: paramsLocal.sourceLabel, rootDir: paramsLocal.rootDir, origin: paramsLocal.origin }); if (!validated.ok) { await paramsLocal.cleanup?.(); return validated; } return { ok: true, marketplace: { manifest: validated.manifest, rootDir: paramsLocal.rootDir, sourceLabel: paramsLocal.sourceLabel, origin: paramsLocal.origin, ...paramsLocal.remoteRef ? { remoteRef: paramsLocal.remoteRef } : {}, cleanup: paramsLocal.cleanup } }; }; const loadResolvedLocalMarketplace = async (local, sourceLabel) => loadMarketplaceFromManifestFile({ manifestPath: local.manifestPath, sourceLabel, rootDir: local.rootDir, origin: "local" }); const resolveClonedMarketplaceManifestPath = async (rootDir) => { for (const candidate of MARKETPLACE_MANIFEST_CANDIDATES) { const next = path.join(rootDir, candidate); if (await pathExists(next)) return next; } }; const knownMarketplaces = await readClaudeKnownMarketplaces(); const visitedKnownMarketplaces = /* @__PURE__ */ new Set(); let source = params.source; while (true) { const known = knownMarketplaces[source]; if (!known) break; if (visitedKnownMarketplaces.has(source)) return { ok: false, error: `known marketplace source cycle: ${[...visitedKnownMarketplaces, source].join(" -> ")}` }; visitedKnownMarketplaces.add(source); if (known.installLocation) { const local = await resolveLocalMarketplaceSource(known.installLocation); if (local?.ok) return await loadResolvedLocalMarketplace(local, source); } const normalizedSource = normalizeEntrySource(known.source); if (!normalizedSource.ok) break; source = marketplaceEntrySourceToInput(normalizedSource.source); } const local = await resolveLocalMarketplaceSource(source); if (local?.ok === false) return local; if (local?.ok) return await loadResolvedLocalMarketplace(local, local.manifestPath); const cloned = await cloneMarketplaceRepo({ source, timeoutMs: params.timeoutMs, logger: params.logger }); if (!cloned.ok) return cloned; const manifestPath = await resolveClonedMarketplaceManifestPath(cloned.rootDir); if (!manifestPath) { await cloned.cleanup(); return { ok: false, error: `marketplace manifest not found in ${cloned.label}` }; } return await loadMarketplaceFromManifestFile({ manifestPath, sourceLabel: cloned.label, rootDir: cloned.rootDir, origin: "remote", ...cloned.ref ? { remoteRef: cloned.ref } : {}, cleanup: cloned.cleanup }); } function resolveSafeMarketplaceDownloadFileName(url, fallback) { const pathname = new URL(url).pathname; const fileName = path.basename(pathname).trim() || fallback; if (fileName === "." || fileName === ".." || /^[a-zA-Z]:/.test(fileName) || path.isAbsolute(fileName) || fileName.includes("/") || fileName.includes("\\")) throw new Error("invalid download filename"); return fileName; } function resolveMarketplaceDownloadTimeoutMs(timeoutMs) { return Math.max(1e3, Math.floor(typeof timeoutMs === "number" && Number.isFinite(timeoutMs) ? timeoutMs : DEFAULT_MARKETPLACE_DOWNLOAD_TIMEOUT_MS)); } function formatMarketplaceDownloadError(url, detail) { return `failed to download ${sanitizeForLog(redactSensitiveUrlLikeString(url))}: ` + sanitizeForLog(detail); } function hasStreamingResponseBody(response) { return Boolean(response.body && typeof response.body.getReader === "function"); } async function cancelUnreadMarketplaceResponseBody(response) { await response.body?.cancel().catch(() => void 0); } function parseMarketplaceContentLength(raw) { const trimmed = raw.trim(); if (!/^\d+$/.test(trimmed)) throw new Error(`invalid content-length header: ${raw}`); const size = Number(trimmed); if (!Number.isSafeInteger(size)) throw new Error(`invalid content-length header: ${raw}`); return size; } async function readMarketplaceChunkWithTimeout(reader, chunkTimeoutMs) { let timeoutId; let timedOut = false; return await new Promise((resolve, reject) => { const clear = () => { if (timeoutId !== void 0) { clearTimeout(timeoutId); timeoutId = void 0; } }; timeoutId = setTimeout(() => { timedOut = true; clear(); reader.cancel().catch(() => void 0); reject(/* @__PURE__ */ new Error(`download timed out after ${chunkTimeoutMs}ms`)); }, chunkTimeoutMs); reader.read().then((result) => { clear(); if (!timedOut) resolve(result); }, (err) => { clear(); if (!timedOut) reject(toErrorObject(err, "Non-Error rejection")); }); }); } async function writeMarketplaceChunk(fileHandle, chunk) { let offset = 0; while (offset < chunk.length) { const { bytesWritten } = await fileHandle.write(chunk, offset, chunk.length - offset); if (bytesWritten <= 0) throw new Error("failed to write download chunk"); offset += bytesWritten; } } async function streamMarketplaceResponseToFile(params) { const reader = params.response.body.getReader(); const fileHandle = await fs.open(params.targetPath, "wx"); let total = 0; try { while (true) { const { done, value } = await readMarketplaceChunkWithTimeout(reader, params.chunkTimeoutMs); if (done) return; if (!value?.length) continue; const nextTotal = total + value.length; if (nextTotal > params.maxBytes) throw new Error(`download too large: ${nextTotal} bytes (limit: ${params.maxBytes} bytes)`); await writeMarketplaceChunk(fileHandle, value); total = nextTotal; } } catch (error) { if (typeof reader.cancel === "function") await reader.cancel().catch(() => void 0); throw error; } finally { await fileHandle.close().catch(() => void 0); try { reader.releaseLock(); } catch {} } } async function downloadUrlToTempFile(url, timeoutMs) { let sourceFileName = "plugin.tgz"; let tmpDir; try { sourceFileName = resolveSafeMarketplaceDownloadFileName(url, sourceFileName); const downloadTimeoutMs = resolveMarketplaceDownloadTimeoutMs(timeoutMs); const { response, finalUrl, release } = await fetchWithSsrFGuard({ url, timeoutMs: downloadTimeoutMs, auditContext: "marketplace-plugin-download" }); try { if (!response.ok) { await cancelUnreadMarketplaceResponseBody(response); return { ok: false, error: formatMarketplaceDownloadError(url, `HTTP ${response.status}`) }; } if (!response.body) return { ok: false, error: formatMarketplaceDownloadError(url, "empty response body") }; if (!hasStreamingResponseBody(response)) { await cancelUnreadMarketplaceResponseBody(response); return { ok: false, error: formatMarketplaceDownloadError(url, "streaming response body unavailable") }; } const contentLength = response.headers.get("content-length"); if (contentLength) { let size; try { size = parseMarketplaceContentLength(contentLength); } catch (error) { await cancelUnreadMarketplaceResponseBody(response); throw error; } if (size > MAX_MARKETPLACE_ARCHIVE_BYTES) { await cancelUnreadMarketplaceResponseBody(response); throw new Error(`download too large: ${size} bytes (limit: ${MAX_MARKETPLACE_ARCHIVE_BYTES} bytes)`); } } const finalFileName = resolveSafeMarketplaceDownloadFileName(finalUrl, sourceFileName); const fileName = resolveArchiveKind(finalFileName) ? finalFileName : sourceFileName; tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-marketplace-download-")); const createdTmpDir = tmpDir; const targetPath = path.resolve(createdTmpDir, fileName); if (!isPathInside(createdTmpDir, targetPath)) throw new Error("invalid download filename"); await streamMarketplaceResponseToFile({ response, targetPath, maxBytes: MAX_MARKETPLACE_ARCHIVE_BYTES, chunkTimeoutMs: downloadTimeoutMs }); return { ok: true, path: targetPath, cleanup: async () => { await fs.rm(createdTmpDir, { recursive: true, force: true }).catch(() => void 0); } }; } finally { await release().catch(() => void 0); } } catch (error) { if (tmpDir) await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => void 0); return { ok: false, error: formatMarketplaceDownloadError(url, formatErrorMessage(error)) }; } } async function ensureInsideMarketplaceRoot(rootDir, candidate, options) { const resolved = path.resolve(rootDir, candidate); const resolvedExists = await pathExists(resolved); if (!isPathInside(rootDir, resolved)) return { ok: false, error: `plugin source escapes marketplace root: ${candidate}` }; if (options?.canonicalRootDir) try { if (!(await fs.lstat(options.canonicalRootDir)).isDirectory()) throw new Error("invalid marketplace root"); const rootRealPath = await fs.realpath(options.canonicalRootDir); let existingPath = resolved; while (!await pathExists(existingPath)) { const parentPath = path.dirname(existingPath); if (parentPath === existingPath) throw new Error("unreachable marketplace path"); existingPath = parentPath; } const existingRealPath = await fs.realpath(existingPath); if (!isPathInside(rootRealPath, existingRealPath)) throw new Error("marketplace path escapes canonical root"); } catch (error) { if (error instanceof Error && (error.message === "invalid marketplace root" || error.message === "unreachable marketplace path" || error.message === "marketplace path escapes canonical root")) return { ok: false, error: `plugin source escapes marketplace root: ${candidate}` }; throw error; } if (!resolvedExists) return { ok: false, error: `plugin source not found in marketplace root: ${candidate}` }; return { ok: true, path: resolved }; } async function validateMarketplaceManifest(params) { if (params.origin === "local") return { ok: true, manifest: params.manifest }; const canonicalRootDir = await fs.realpath(params.rootDir); for (const plugin of params.manifest.plugins) { const source = plugin.source; if (source.kind === "path") { if (hasHttpUrlPrefix(source.path)) return { ok: false, error: `invalid marketplace entry "${plugin.name}" in ${params.sourceLabel}: remote marketplaces may not use HTTP(S) plugin paths` }; if (path.isAbsolute(source.path)) return { ok: false, error: `invalid marketplace entry "${plugin.name}" in ${params.sourceLabel}: remote marketplaces may only use relative plugin paths` }; const resolved = await ensureInsideMarketplaceRoot(params.rootDir, source.path, { canonicalRootDir }); if (!resolved.ok) return { ok: false, error: `invalid marketplace entry "${plugin.name}" in ${params.sourceLabel}: ${resolved.error}` }; continue; } return { ok: false, error: `invalid marketplace entry "${plugin.name}" in ${params.sourceLabel}: remote marketplaces may not use ${source.kind} plugin sources` }; } return { ok: true, manifest: params.manifest }; } async function resolveMarketplaceEntryInstallPath(params) { if (params.source.kind === "path") { if (hasHttpUrlPrefix(params.source.path)) { if (resolveArchiveKind(params.source.path)) return await downloadUrlToTempFile(params.source.path, params.timeoutMs); return { ok: false, error: `unsupported remote plugin path source: ${params.source.path}` }; } const canonicalRootDir = params.marketplaceOrigin === "remote" ? await fs.realpath(params.marketplaceRootDir) : void 0; const resolved = path.isAbsolute(params.source.path) ? { ok: true, path: params.source.path } : await ensureInsideMarketplaceRoot(params.marketplaceRootDir, params.source.path, { canonicalRootDir }); if (!resolved.ok) return resolved; return { ok: true, path: resolved.path }; } if (params.source.kind === "github" || params.source.kind === "git" || params.source.kind === "git-subdir") { const cloned = await cloneMarketplaceRepo({ source: params.source.kind === "github" ? `${params.source.repo}${params.source.ref ? `#${params.source.ref}` : ""}` : `${params.source.url}${params.source.ref ? `#${params.source.ref}` : ""}`, timeoutMs: params.timeoutMs, logger: params.logger }); if (!cloned.ok) return cloned; const subPath = params.source.kind === "github" || params.source.kind === "git" ? normalizeOptionalString(params.source.path) || "." : params.source.path.trim(); const canonicalRootDir = await fs.realpath(cloned.rootDir); const target = await ensureInsideMarketplaceRoot(cloned.rootDir, subPath, { canonicalRootDir }); if (!target.ok) { await cloned.cleanup(); return target; } return { ok: true, path: target.path, cleanup: cloned.cleanup }; } if (resolveArchiveKind(params.source.url)) return await downloadUrlToTempFile(params.source.url, params.timeoutMs); if (!normalizeGitCloneSource(params.source.url)) return { ok: false, error: `unsupported URL plugin source: ${params.source.url}` }; const cloned = await cloneMarketplaceRepo({ source: params.source.url, timeoutMs: params.timeoutMs, logger: params.logger }); if (!cloned.ok) return cloned; return { ok: true, path: cloned.rootDir, cleanup: cloned.cleanup }; } async function listMarketplacePlugins(params) { const loaded = await loadMarketplace({ source: params.marketplace, logger: params.logger, timeoutMs: params.timeoutMs }); if (!loaded.ok) return loaded; try { return { ok: true, manifest: loaded.marketplace.manifest, sourceLabel: loaded.marketplace.sourceLabel }; } finally { await loaded.marketplace.cleanup?.(); } } async function resolveMarketplaceInstallShortcut(raw) { const trimmed = raw.trim(); const atIndex = trimmed.lastIndexOf("@"); if (atIndex <= 0 || atIndex >= trimmed.length - 1) return null; const plugin = trimmed.slice(0, atIndex).trim(); const marketplaceName = trimmed.slice(atIndex + 1).trim(); if (!plugin || !marketplaceName || plugin.includes("/")) return null; const known = (await readClaudeKnownMarketplaces())[marketplaceName]; if (!known) return null; if (known.installLocation) return { ok: true, plugin, marketplaceName, marketplaceSource: marketplaceName }; const normalizedSource = normalizeEntrySource(known.source); if (!normalizedSource.ok) return { ok: false, error: `known Claude marketplace "${marketplaceName}" has an invalid source: ${normalizedSource.error}` }; return { ok: true, plugin, marketplaceName, marketplaceSource: marketplaceName }; } async function installPluginFromMarketplace(params) { const loaded = await loadMarketplace({ source: params.marketplace, logger: params.logger, timeoutMs: params.timeoutMs }); if (!loaded.ok) return loaded; let installCleanup; try { const entry = loaded.marketplace.manifest.plugins.find((plugin) => plugin.name === params.plugin); if (!entry) { const known = loaded.marketplace.manifest.plugins.map((plugin) => plugin.name).toSorted(); return { ok: false, error: `plugin "${params.plugin}" not found in marketplace ${loaded.marketplace.sourceLabel}` + (known.length > 0 ? ` (available: ${known.join(", ")})` : "") }; } const resolved = await resolveMarketplaceEntryInstallPath({ source: entry.source, marketplaceRootDir: loaded.marketplace.rootDir, marketplaceOrigin: loaded.marketplace.origin, logger: params.logger, timeoutMs: params.timeoutMs }); if (!resolved.ok) return resolved; installCleanup = resolved.cleanup; const result = await installPluginFromPath(copyPluginInstallTransactionRequest(params, { dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall, onInstallPolicyWarning: params.onInstallPolicyWarning, config: params.config, path: resolved.path, logger: params.logger, mode: params.mode, extensionsDir: params.extensionsDir, timeoutMs: params.timeoutMs, dryRun: params.dryRun, expectedPluginId: params.expectedPluginId, onBeforePluginArtifactCommit: params.onBeforePluginArtifactCommit, beforePersistentApply: params.beforePersistentApply, installPolicyRequest: { kind: marketplaceInstallPolicyRequestKind({ marketplaceOrigin: loaded.marketplace.origin, resolvedPath: resolved.path, source: entry.source }), requestedSpecifier: `${entry.name}@${params.marketplace}`, source: marketplaceInstallPolicySource({ marketplaceOrigin: loaded.marketplace.origin, marketplaceRef: loaded.marketplace.remoteRef, resolvedPath: resolved.path, source: entry.source }) } })); if (!result.ok) return result; return { ...result, marketplaceName: loaded.marketplace.manifest.name, marketplaceVersion: loaded.marketplace.manifest.version, marketplacePlugin: entry.name, marketplaceSource: params.marketplace, marketplaceEntryVersion: entry.version }; } finally { await installCleanup?.(); await loaded.marketplace.cleanup?.(); } } //#endregion export { listMarketplacePlugins as n, resolveMarketplaceInstallShortcut as r, installPluginFromMarketplace as t };