import { readdir, stat } from "node:fs/promises"; import { existsSync } from "node:fs"; import { join, relative, basename, sep } from "node:path"; import type { SourceModule } from "../registry"; import { ObsidianConfig, ObsidianPayload, type ObsidianConfig as Config, type ObsidianPayload as Payload, } from "@/lib/schemas/sources/obsidian"; const SKIP_DIRS = new Set([".obsidian", ".trash", ".git", "node_modules"]); function vaultPath(config: Config): string { return config.vault || process.env.OBSIDIAN_VAULT || ""; } export const obsidianModule: SourceModule = { kind: "obsidian", label: "Obsidian", keyless: true, defaultRefreshSeconds: 600, configSchema: ObsidianConfig, payloadSchema: ObsidianPayload, isConfigured: (config) => { const v = vaultPath(config); return v.length > 0 && existsSync(v); }, // Read-only: we only stat/list note files. Never write to the vault. async fetch({ config }) { const vault = vaultPath(config); if (!vault || !existsSync(vault)) { throw new Error("Vault path not found — set it in Settings."); } const entries = await readdir(vault, { recursive: true, withFileTypes: true }); const mdFiles = entries.filter((e) => { if (!e.isFile() || !e.name.endsWith(".md")) return false; const parent = (e as { parentPath?: string; path?: string }).parentPath ?? (e as { path?: string }).path ?? vault; const rel = relative(vault, join(parent, e.name)); return !rel.split(sep).some((seg) => SKIP_DIRS.has(seg)); }); const notes = await Promise.all( mdFiles.map(async (e) => { const parent = (e as { parentPath?: string; path?: string }).parentPath ?? (e as { path?: string }).path ?? vault; const full = join(parent, e.name); const s = await stat(full); return { title: basename(e.name, ".md"), path: relative(vault, full), modified: s.mtime.toISOString(), }; }), ); notes.sort((a, b) => Date.parse(b.modified) - Date.parse(a.modified)); return { vault, notes: notes.slice(0, config.limit) }; }, };