{"record":{"id":"ee7de446b440a04b","repo":"can1357/oh-my-pi","slug":"vault-url-must-resolve-to-a-directory-parsed","errorCode":null,"errorMessage":"vault:// URL must resolve to a directory: ${parsed.url}","messagePattern":"vault:// URL must resolve to a directory: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/vault-protocol.ts","lineNumber":834,"sourceCode":"\t\tcontext?: ResolveContext | WriteContext,\n\t): Promise<{ root: string; targetPath: string }> {\n\t\tconst root = await this.#resolveVaultRoot(parsed.ref, context);\n\t\tconst resolvedRoot = await fs.promises.realpath(root);\n\t\tconst targetPath = parsed.relativePath ? path.resolve(resolvedRoot, parsed.relativePath) : resolvedRoot;\n\t\tensureWithinRoot(targetPath, resolvedRoot);\n\t\treturn { root: resolvedRoot, targetPath };\n\t}\n\n\tasync #listDir(\n\t\tparsed: Extract<ParsedVaultUrl, { kind: \"fs-dir\" | \"fs-file\" }>,\n\t\tcontext?: ResolveContext,\n\t): Promise<InternalResource> {\n\t\tconst { root, targetPath } = await this.#resolveFsTarget(parsed, context);\n\t\tconst realTargetPath = await fs.promises.realpath(targetPath);\n\t\tensureWithinRoot(realTargetPath, root);\n\t\tconst stat = await fs.promises.stat(realTargetPath);\n\t\tif (!stat.isDirectory()) {\n\t\t\tthrow new Error(`vault:// URL must resolve to a directory: ${parsed.url}`);\n\t\t}\n\t\tconst entries = await fs.promises.readdir(realTargetPath, { withFileTypes: true });\n\t\tentries.sort((a, b) => a.name.localeCompare(b.name));\n\t\tconst baseRelative = parsed.relativePath ? `${parsed.relativePath}/` : \"\";\n\t\tconst lines = entries.map(entry => {\n\t\t\tconst entryRelativePath = `${baseRelative}${entry.name}`;\n\t\t\tconst isDir = entry.isDirectory();\n\t\t\tconst href = formatVaultPathForLink(parsed.ref, entryRelativePath, isDir);\n\t\t\treturn `- [${entry.name}${isDir ? \"/\" : \"\"}](${href})`;\n\t\t});\n\t\tconst listing = lines.length === 0 ? \"(empty)\" : lines.join(\"\\n\");\n\t\tconst titlePath = parsed.relativePath ? `/${parsed.relativePath}/` : \"/\";\n\t\tconst content = `# Vault ${parsed.ref.display}${titlePath}\\n\\n${entries.length} entr${entries.length === 1 ? \"y\" : \"ies\"}:\\n\\n${listing}\\n`;\n\t\treturn {\n\t\t\turl: parsed.url,\n\t\t\tcontent,\n\t\t\tcontentType: \"text/markdown\",\n\t\t\tsize: Buffer.byteLength(content, \"utf-8\"),","sourceCodeStart":816,"sourceCodeEnd":852,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/vault-protocol.ts#L816-L852","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Drop the directory form and resolve the URL as a file (no trailing slash) if the target is a file.","Verify the on-disk path is actually a directory (ls/stat) and correct the URL path.","Use the directory listing error to discover the correct target name, then rebuild the URL."],"exampleFix":"// before\nawait resolveInternalUrl(\"vault://_/notes/\"); // notes is actually a file\n// after\nawait resolveInternalUrl(\"vault://_/notes\"); // resolves as a file","handlingStrategy":"type-guard","validationCode":"import * as fs from \"node:fs\";\nconst stat = fs.statSync(targetPath, { throwIfNoEntry: false });\nif (stat && !stat.isDirectory()) throw new Error(`${targetPath} is a file; drop the directory form of the vault:// URL`);","typeGuard":"function isDirectoryStat(stat: fs.Stats | undefined): stat is fs.Stats & { isDirectory(): true } {\n  return !!stat && stat.isDirectory();\n}","tryCatchPattern":"try {\n  return await resolveInternalUrl(dirUrl);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"must resolve to a directory\")) {\n    return resolveInternalUrl(dirUrl.replace(/\\/$/, \"\")); // retry as file\n  }\n  throw err;\n}","preventionTips":["Don't add trailing slashes to vault:// URLs unless you know the target is a directory.","Stat ambiguous extensionless paths before choosing dir vs file URL form.","Handle extensionless files explicitly in path-building code."],"tags":["vault","filesystem","kind-mismatch"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}