/** * Field builders shared by every note type. * * Each field carries `title` and `description` metadata. The JSON Schema * export keeps them, so the MCP server, the admin page and any later editor * can render a form or explain a field without a second definition. Image * and link fields also carry a `format` (`vault-image`, `vault-link`, * `vault-links`) so a form knows to offer a picker. */ import { z } from "zod"; import { normalizeWikilinkInput, wikilinkTarget } from "./names.ts"; type Meta = { title: string; description?: string }; /** A calendar date written `YYYY-MM-DD`, as Obsidian writes dates. */ export const isoDate = (meta: Meta) => z .preprocess( (v) => (v instanceof Date ? v.toISOString().slice(0, 10) : v), z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use a date like 2026-10-11"), ) .meta(meta); /** A month written `YYYY-MM`, for resume dates. */ export const isoMonth = (meta: Meta) => z.string().regex(/^\d{4}-\d{2}$/, "Use a month like 2019-03").meta(meta); /** A link to another note, written `[[Note name]]`. */ export const wikilink = (meta: Meta) => z .preprocess( normalizeWikilinkInput, z.string().refine((v) => wikilinkTarget(v) !== null, "Write a link as [[Note name]]"), ) .meta({ format: "vault-link", ...meta }); /** A list of links to other notes. */ export const wikilinks = (meta: Meta) => z .preprocess( (v) => (Array.isArray(v) ? v.map(normalizeWikilinkInput) : v), z.array(z.string().refine((v) => wikilinkTarget(v) !== null, "Write each link as [[Note name]]")), ) .meta({ format: "vault-links", ...meta }); /** An image: a vault attachment as `[[file.jpg]]`, or a full https URL. */ export const image = (meta: Meta) => z .preprocess( normalizeWikilinkInput, z .string() .refine( (v) => wikilinkTarget(v) !== null || /^https:\/\//.test(v), "Use a vault image as [[file.jpg]] or a full https URL", ), ) .meta({ format: "vault-image", ...meta }); /** One short line of text. */ export const line = (meta: Meta) => z.string().trim().min(1).meta(meta); /** A list of short tags or names. */ export const strings = (meta: Meta) => z.array(z.string().trim().min(1)).meta(meta); /** * Fields every note shares. `type` is added per note type. * * `publish` defaults to false: a note goes live only when it says so. */ export const baseFields = { title: line({ title: "Title" }), slug: z .string() .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Lowercase words joined by hyphens") .meta({ title: "Slug", description: "The last part of the page address. Made from the title when left out." }) .optional(), publish: z .boolean() .default(false) .meta({ title: "Publish", description: "Only notes set to true appear on the site." }), favorite: z.boolean().meta({ title: "Favorite", description: "Starred on the site." }).optional(), tags: strings({ title: "Tags" }).optional(), links: wikilinks({ title: "Links", description: "Other notes this one is about. The site shows the link on both notes.", }).optional(), cover: image({ title: "Cover image" }).optional(), created: isoDate({ title: "Created" }).optional(), updated: isoDate({ title: "Updated" }).optional(), updatedBy: z .enum(["greg", "agent", "admin", "sync"]) .meta({ title: "Last edited by", description: "Set by the content service; you never need to write it." }) .optional(), agentEdits: z .boolean() .meta({ title: "Agents may edit", description: "Let agents change this note without your review." }) .optional(), };