from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path from typing import Iterator @dataclass(frozen=True) class VaultPaths: root: str notes: str attachments: str kb: str class Vault: def __init__(self, root_path: str): root = Path(root_path).expanduser().resolve() object.__setattr__( self, "paths", VaultPaths( root=str(root), notes=str(root / "notes"), attachments=str(root / "attachments"), kb=str(root / ".kb"), ), ) def ensure_structure(self) -> None: Path(self.paths.notes).mkdir(parents=True, exist_ok=True) Path(self.paths.attachments).mkdir(parents=True, exist_ok=True) Path(self.paths.kb).mkdir(parents=True, exist_ok=True) def abspath(self, rel_path: str) -> str: # If rel_path is absolute, Path(root) / rel_path will return rel_path as-is. return str((Path(self.paths.root) / rel_path).resolve()) def relpath(self, abs_path: str) -> str: return str(Path(abs_path).resolve().relative_to(Path(self.paths.root).resolve())) def iter_markdown_files(self) -> Iterator[str]: """ Yield absolute paths to .md files under /notes recursively. - Allows the vault root to be hidden or not. - Skips hidden subdirectories within notes/ (names starting with '.'). - Skips hidden files (names starting with '.'). """ notes_dir = Path(self.paths.notes) if not notes_dir.exists(): return iter(()) # Walk manually to filter hidden dirs/files for dirpath, dirnames, filenames in os.walk(notes_dir): # Remove hidden subdirectories in-place (prevents os.walk from entering them) dirnames[:] = [d for d in dirnames if not d.startswith(".")] for fname in filenames: if fname.startswith("."): continue if not fname.endswith(".md"): continue yield str(Path(dirpath, fname).resolve())