can1357/oh-my-pi · error · Error

Cannot resolve relative source "${source}": marketplaceClone

Error message

Cannot resolve relative source "${source}": marketplaceClonePath is required

What it means

Relative plugin sources are resolved against the marketplace's local clone directory. resolveRelativeSource() reads that directory from context.marketplaceClonePath; when the caller did not supply one (no clone exists or the context was built without it), it throws this error because the relative path has nothing to resolve against.

Source

Thrown at packages/coding-agent/src/extensibility/plugins/marketplace/source-resolver.ts:60

	if (typeof source === "string") {
		return resolveRelativeSource(source, context);
	}

	return resolveObjectSource(source, context);
}

// ── Relative string source ("./plugins/foo") ────────────────────────

async function resolveRelativeSource(
	source: string,
	context: ResolveContext,
): Promise<{ dir: string; tempCloneRoot?: string }> {
	if (!source.startsWith("./")) {
		throw new Error(`Relative plugin source paths must start with "./" — got: "${source}"`);
	}

	if (!context.marketplaceClonePath) {
		throw new Error(`Cannot resolve relative source "${source}": marketplaceClonePath is required`);
	}

	// If pluginRoot is set, prepend it to the path segment after "./"
	const pluginRoot = context.catalogMetadata?.pluginRoot;
	const relativePath = pluginRoot ? `./${path.join(pluginRoot, source.slice(2))}` : source;

	// Resolve against marketplace root (not the .claude-plugin/ catalog subdirectory)
	const resolved = path.resolve(context.marketplaceClonePath, relativePath);

	if (!pathIsWithin(context.marketplaceClonePath, resolved)) {
		throw new Error(
			`Plugin source "${source}" resolves outside marketplace root ("${context.marketplaceClonePath}")`,
		);
	}

	await verifyDirExists(resolved, `Plugin source directory does not exist: "${resolved}"`);
	return { dir: resolved };
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Run the marketplace update/clone step first so marketplaceClonePath exists, then retry the install
  2. Re-add the marketplace to force a fresh clone if the directory was deleted
  3. If embedding the resolver, pass marketplaceClonePath in the ResolveContext
  4. Check for a prior clone failure (network/disk) that left the context incomplete

Example fix

// before
const dir = await resolvePluginSource("./plugins/foo", { catalogMetadata }); // no clonePath
// after
const dir = await resolvePluginSource("./plugins/foo", {
  marketplaceClonePath: "~/.omp/marketplaces/community",
  catalogMetadata,
});
Defensive patterns

Strategy: validation

Validate before calling

function canResolveRelative(ctx) {
  return Boolean(ctx.marketplaceClonePath);
}
if (!canResolveRelative(ctx)) await cloneOrUpdateMarketplace(marketplaceName);

Type guard

const hasClonePath = (ctx) => typeof ctx?.marketplaceClonePath === 'string' && ctx.marketplaceClonePath.length > 0;

Try / catch

try {
  const dir = await resolvePluginSource(source, ctx);
} catch (err) {
  if (err.message.includes('marketplaceClonePath is required')) {
    await updateMarketplace(marketplaceName); // materialize the clone
    // rebuild ctx and retry once
  } else throw err;
}

Prevention

When it happens

Trigger: Calling resolvePluginSource() for a "./..." source with a ResolveContext lacking marketplaceClonePath — e.g. resolving before the marketplace was cloned/updated, or constructing the context manually and omitting the field after a failed clone.

Common situations: Installing a plugin from a marketplace whose clone directory was deleted; a marketplace update failed leaving no local clone; SDK/API usage building ResolveContext by hand without the clone path; running an install before the first marketplace sync.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/6af671647b04c4e7. Report an issue: GitHub.