can1357/oh-my-pi · error

vault:// URL must resolve to a directory: ${parsed.url}

Error message

vault:// URL must resolve to a directory: ${parsed.url}

What it means

When resolving a vault:// URL that was parsed as a directory (#readDirResource), the handler stats the real target path and requires it to actually be a directory. If the path exists but is a regular file (or other non-directory), this error is thrown with the original URL in the message. It catches mismatches between the URL's implied kind and the on-disk reality.

Source

Thrown at packages/coding-agent/src/internal-urls/vault-protocol.ts:834

		context?: ResolveContext | WriteContext,
	): Promise<{ root: string; targetPath: string }> {
		const root = await this.#resolveVaultRoot(parsed.ref, context);
		const resolvedRoot = await fs.promises.realpath(root);
		const targetPath = parsed.relativePath ? path.resolve(resolvedRoot, parsed.relativePath) : resolvedRoot;
		ensureWithinRoot(targetPath, resolvedRoot);
		return { root: resolvedRoot, targetPath };
	}

	async #listDir(
		parsed: Extract<ParsedVaultUrl, { kind: "fs-dir" | "fs-file" }>,
		context?: ResolveContext,
	): Promise<InternalResource> {
		const { root, targetPath } = await this.#resolveFsTarget(parsed, context);
		const realTargetPath = await fs.promises.realpath(targetPath);
		ensureWithinRoot(realTargetPath, root);
		const stat = await fs.promises.stat(realTargetPath);
		if (!stat.isDirectory()) {
			throw new Error(`vault:// URL must resolve to a directory: ${parsed.url}`);
		}
		const entries = await fs.promises.readdir(realTargetPath, { withFileTypes: true });
		entries.sort((a, b) => a.name.localeCompare(b.name));
		const baseRelative = parsed.relativePath ? `${parsed.relativePath}/` : "";
		const lines = entries.map(entry => {
			const entryRelativePath = `${baseRelative}${entry.name}`;
			const isDir = entry.isDirectory();
			const href = formatVaultPathForLink(parsed.ref, entryRelativePath, isDir);
			return `- [${entry.name}${isDir ? "/" : ""}](${href})`;
		});
		const listing = lines.length === 0 ? "(empty)" : lines.join("\n");
		const titlePath = parsed.relativePath ? `/${parsed.relativePath}/` : "/";
		const content = `# Vault ${parsed.ref.display}${titlePath}\n\n${entries.length} entr${entries.length === 1 ? "y" : "ies"}:\n\n${listing}\n`;
		return {
			url: parsed.url,
			content,
			contentType: "text/markdown",
			size: Buffer.byteLength(content, "utf-8"),

View on GitHub (pinned to 9690622007)

Solutions

  1. Drop the directory form and resolve the URL as a file (no trailing slash) if the target is a file.
  2. Verify the on-disk path is actually a directory (ls/stat) and correct the URL path.
  3. Use the directory listing error to discover the correct target name, then rebuild the URL.

Example fix

// before
await resolveInternalUrl("vault://_/notes/"); // notes is actually a file
// after
await resolveInternalUrl("vault://_/notes"); // resolves as a file
Defensive patterns

Strategy: type-guard

Validate before calling

import * as fs from "node:fs";
const stat = fs.statSync(targetPath, { throwIfNoEntry: false });
if (stat && !stat.isDirectory()) throw new Error(`${targetPath} is a file; drop the directory form of the vault:// URL`);

Type guard

function isDirectoryStat(stat: fs.Stats | undefined): stat is fs.Stats & { isDirectory(): true } {
  return !!stat && stat.isDirectory();
}

Try / catch

try {
  return await resolveInternalUrl(dirUrl);
} catch (err) {
  if (err instanceof Error && err.message.includes("must resolve to a directory")) {
    return resolveInternalUrl(dirUrl.replace(/\/$/, "")); // retry as file
  }
  throw err;
}

Prevention

When it happens

Trigger: Resolving a vault:// URL as a directory (parsed kind fs-dir / trailing-slash or dir-kind parse) when the target path exists on disk as a regular file, e.g. vault://_/notes where notes is a file named "notes" with no extension.

Common situations: URLs built with a trailing slash from stale templates after the target changed from directory to file; ambiguous extensionless paths guessed to be directories; symlinks resolving to files.

Related errors


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