Skip to content

Latest commit

 

History

History
921 lines (674 loc) · 28.3 KB

File metadata and controls

921 lines (674 loc) · 28.3 KB

Vulpea API Reference

This reference covers the programmatic API for building tools with Vulpea.

The vulpea-note Struct

All query functions return vulpea-note structs:

(vulpea-note-id note)            ; UUID string
(vulpea-note-path note)          ; Absolute file path
(vulpea-note-level note)         ; 0 = file-level, 1+ = heading
(vulpea-note-title note)         ; Note title
(vulpea-note-primary-title note) ; Primary title (without aliases)
(vulpea-note-aliases note)       ; List of alias strings
(vulpea-note-tags note)          ; List of tag strings
(vulpea-note-links note)         ; List of (type . target) pairs
(vulpea-note-meta note)          ; Alist of metadata
(vulpea-note-properties note)    ; Alist of org properties
(vulpea-note-todo note)          ; TODO state string or nil
(vulpea-note-priority note)      ; Priority character or nil
(vulpea-note-scheduled note)     ; Scheduled timestamp or nil
(vulpea-note-deadline note)      ; Deadline timestamp or nil
(vulpea-note-closed note)        ; Closed timestamp or nil
(vulpea-note-outline-path note)  ; List of parent headings
(vulpea-note-attach-dir note)    ; Attachment directory or nil
(vulpea-note-file-title note)    ; Title of file containing note

Getting Notes

By ID

The fastest way to get a single note:

(vulpea-db-get-by-id "5093fc4e-8c63-4e60-a1da-83fc7ecd5db7")
;; => #s(vulpea-note :id "5093fc4e..." :title "My Note" ...)
;; => nil if not found

Multiple by IDs

(vulpea-db-query-by-ids '("id-1" "id-2" "id-3"))
;; => list of vulpea-note structs (missing IDs are omitted)

File Path for ID

(vulpea-db-get-file-by-id "note-id")
;; => "/path/to/note.org" or nil

By Title Search

Find notes matching a title substring:

(vulpea-db-search-by-title "meeting")
;; => list of notes with "meeting" in title

Query Functions

All query functions return lists of vulpea-note structs.

By Tags

;; Notes with ANY of these tags
(vulpea-db-query-by-tags-some '("project" "task"))

;; Notes with ALL of these tags
(vulpea-db-query-by-tags-every '("project" "active"))

;; Notes WITHOUT any of these tags
(vulpea-db-query-by-tags-none '("archive" "deprecated"))

By Links

;; Notes linking to ANY of these IDs (backlinks)
(vulpea-db-query-by-links-some '("target-note-id"))

;; Filter by link type
(vulpea-db-query-by-links-some '("note-id") "id")

;; Notes linking to ALL of these IDs
(vulpea-db-query-by-links-every '("id-1" "id-2"))

By Attachments

;; Get all attachment destinations with their attach-dirs for notes in a file
(vulpea-db-query-attachments-by-path "/path/to/file.org")
;; => (("image.png" . "/data/note1/") ("document.pdf" . "/data/note2/") ...)

This is an optimized query that retrieves all attachment link destinations along with their attachment directories in a single database query, avoiding N+1 queries when processing multiple notes in a file. Each result is a cons cell (DEST . ATTACH-DIR) where DEST is the attachment file name and ATTACH-DIR is the attachment directory of the note containing the link.

By Metadata

;; Notes with a specific metadata key
(vulpea-db-query-by-meta-key "status")

;; Notes where key equals value
(vulpea-db-query-by-meta "status" "active")

;; With type specification
(vulpea-db-query-by-meta "region" "region-id" "note")

By File Paths

;; Notes from specific files
(vulpea-db-query-by-file-paths '("/path/to/a.org" "/path/to/b.org"))

;; With level filter
(vulpea-db-query-by-file-paths '("/path/to/a.org") 0)

By Level

;; File-level notes only
(vulpea-db-query-by-level 0)

;; First-level headings only
(vulpea-db-query-by-level 1)

Custom Predicate

For complex queries:

(vulpea-db-query
 (lambda (note)
   (and (member "project" (vulpea-note-tags note))
        (string-prefix-p "2025" (vulpea-note-title note)))))

Get All Tags

(vulpea-db-query-tags)
;; => ("project" "task" "meeting" ...)

Diagnostics

Dead Links

Find broken ID links that point to non-existent notes:

(vulpea-db-query-dead-links)
;; => ((#s(vulpea-note ...) . "nonexistent-id") ...)

Returns list of cons cells (SOURCE-NOTE . BROKEN-TARGET-ID). Only checks links of type “id”.

Orphan Notes

Find notes with no incoming ID links (nothing links to them):

(vulpea-db-query-orphan-notes)
;; => (#s(vulpea-note ...) ...)

Isolated Notes

Find notes with no incoming or outgoing ID links (completely disconnected):

(vulpea-db-query-isolated-notes)
;; => (#s(vulpea-note ...) ...)

This is stricter than vulpea-db-query-orphan-notes — a note that links out but has no incoming links is an orphan but not isolated.

Title Collisions

Find notes that share the same title:

;; All notes
(vulpea-db-query-title-collisions)
;; => (("Wine" . (#s(vulpea-note ...) #s(vulpea-note ...)))
;;     ("Beer" . (#s(vulpea-note ...) #s(vulpea-note ...))))

;; File-level notes only
(vulpea-db-query-title-collisions 0)

Returns list of (TITLE . NOTES) groups where each group has two or more notes with the same title. Optional LEVEL argument restricts to notes at a specific heading level (0 for file-level).

Link Queries

Query links directly from the database. All functions return plists with :source, :dest, :type, and :pos keys.

;; All links
(vulpea-db-query-links)
;; => ((:source "id1" :dest "id2" :type "id" :pos 100) ...)

;; Links of a specific type
(vulpea-db-query-links-by-type "id")
(vulpea-db-query-links-by-type "https")

;; Outgoing links from a note
(vulpea-db-query-links-from "note-id")

;; Incoming links (backlinks) to a note
(vulpea-db-query-links-to "note-id")

Statistics

(vulpea-db-count-notes)               ; Total notes
(vulpea-db-count-file-level-notes)    ; File-level only
(vulpea-db-count-heading-level-notes) ; Heading-level only

Reading Metadata

Single Value

;; Default: returns string
(vulpea-note-meta-get note "country")
;; => "France"

;; As number
(vulpea-note-meta-get note "rating" 'number)
;; => 95

;; As symbol
(vulpea-note-meta-get note "status" 'symbol)
;; => 'active

;; As link (extracts ID or URL)
(vulpea-note-meta-get note "url" 'link)
;; => "https://example.com"

;; As note (resolves id: link to vulpea-note)
(vulpea-note-meta-get note "region" 'note)
;; => #s(vulpea-note :id "region-id" :title "Bordeaux" ...)

Multiple Values

When a key has multiple entries:

- grape :: Cabernet Sauvignon
- grape :: Merlot
(vulpea-note-meta-get-list note "grape")
;; => ("Cabernet Sauvignon" "Merlot")

(vulpea-note-meta-get-list note "ratings" 'number)
;; => (95 92 88)

Access Patterns

There are multiple ways to read metadata, each with different trade-offs:

FunctionSourceSpeedAlways Fresh?
vulpea-note-meta-getNote struct (from DB)FastNo (needs sync)
vulpea-meta-getFile on diskSlowerYes
vulpea-buffer-meta-get!Pre-parsed metadataFastestDepends

Prefer =vulpea-note-meta-get= for most cases - it reads from the note struct which is populated from the database:

(vulpea-note-meta-get note "status")

Use =vulpea-meta-get= when you need to read directly from the file (always fresh, but slower):

(vulpea-meta-get note-or-id "status")

Use bang functions when reading multiple values from the same note via file access. Parse once and reuse:

;; Inefficient: parses file 3 times
(vulpea-meta-get note "a")
(vulpea-meta-get note "b")
(vulpea-meta-get note "c")

;; Efficient: parses file once
(let ((meta (vulpea-meta note)))
  (vulpea-buffer-meta-get! meta "a")
  (vulpea-buffer-meta-get! meta "b")
  (vulpea-buffer-meta-get! meta "c"))

Creating Notes

File-level notes

;; Simple
(vulpea-create "My Note")
;; => #s(vulpea-note ...)

;; With options
(vulpea-create "Project Note"
               nil                              ; file-name (nil = use template)
               :tags '("project" "work")
               :properties '(("STATUS" . "active"))
               :meta '(("client" . "Acme Corp"))
               :head "#+created: %<[%Y-%m-%d]>"
               :body "* Tasks\n\n* Notes\n")

Returns the created vulpea-note. Signals an error if a file already exists at the target path (to prevent accidental overwrites).

Heading-level notes

Use :parent to create a heading inside an existing note’s file:

;; Create a heading under an existing note
(let ((parent (vulpea-db-get-by-id "parent-id")))
  (vulpea-create "Sub-heading"
                 nil
                 :parent parent
                 :tags '("journal")
                 :properties '(("CREATED" . "[2025-01-15]"))))

;; Insert as first child
(vulpea-create "First Item" nil
               :parent parent-note
               :after nil)

;; Insert after a specific sibling
(vulpea-create "After Sibling" nil
               :parent parent-note
               :after "sibling-note-id")

The heading level is computed automatically as parent-level + 1. The parent’s file must exist. Returns the created vulpea-note.

Parameters

ParameterDescriptionDefault
titleNote title (required)
file-nameFile path relative to default directoryfrom template
:idUUID for the noteauto-generated
:tagsList of tags
:headContent after #+title:
:bodyNote body content
:propertiesAlist for property drawer
:metaAlist for metadata
:contextCustom variables for template expansion
:parentvulpea-note to create heading undernil (file-level)
:afterInsertion position among siblings'last

:after accepts:

  • 'last — append as last child (default)
  • nil — insert as first child
  • string — insert after the sibling with that note ID

Visiting Notes

;; By ID
(vulpea-visit "note-id")

;; By note struct
(vulpea-visit note)

;; In other window
(vulpea-visit note t)

Buffer Functions

These operate on the current buffer (must be an org file).

Title

(vulpea-buffer-title-get)              ; Get title
(vulpea-buffer-title-set "New Title")  ; Set title

Tags

(vulpea-buffer-tags-get)               ; Get tags
(vulpea-buffer-tags-set '("a" "b"))    ; Set tags
(vulpea-buffer-tags-add "new-tag")     ; Add tag (interactive)
(vulpea-buffer-tags-remove "old-tag")  ; Remove tag (interactive)

Aliases

(vulpea-buffer-alias-get)              ; Get aliases
(vulpea-buffer-alias-set "A" "B")      ; Set aliases (replaces all)
(vulpea-buffer-alias-add "Alias")      ; Add alias
(vulpea-buffer-alias-remove "Alias")   ; Remove alias

Metadata

;; Set metadata (in current buffer)
(vulpea-buffer-meta-set "key" "value")
(vulpea-buffer-meta-set "rating" "95")

;; Set multiple properties efficiently (parses buffer once)
(vulpea-buffer-meta-set-batch
 '(("status" . "active")
   ("priority" . 1)
   ("tags" . ("a" "b" "c"))))

;; Sort metadata keys in a specific order
(vulpea-buffer-meta-sort '("status" "priority" "tags"))
;; Keys not in the list are appended at the end

;; File-level version (by note or ID)
(vulpea-meta-set-batch note
 '(("status" . "done")
   ("reviewed" . t)))

When setting multiple metadata properties, prefer vulpea-meta-set-batch over multiple vulpea-meta-set calls - it’s significantly faster (up to 10x for 20 properties) since it only parses the file once.

Heading-Level Metadata

Both file-level and heading-level notes can have their own metadata. The metadata API automatically scopes operations to the correct level based on the note’s level attribute.

For file-level notes (level = 0), metadata is the first description list before any headings:

:PROPERTIES:
:ID:       file-id
:END:
#+title: My Note

- status :: active
- count :: 42

Content here...

* Some Heading
:PROPERTIES:
:ID:       heading-id
:END:

- status :: done

Heading content...

For heading-level notes (level > 0), metadata is the first description list within that heading’s subtree:

;; Get the file-level note
(let ((file-note (vulpea-db-get-by-id "file-id")))
  (vulpea-meta-get file-note "status"))
;; => "active"

;; Get the heading-level note
(let ((heading-note (vulpea-db-get-by-id "heading-id")))
  (vulpea-meta-get heading-note "status"))
;; => "done"

;; Each note has its own metadata scope
(vulpea-meta-set file-note "status" "review")    ; Only affects file-level
(vulpea-meta-set heading-note "status" "pending") ; Only affects heading

This scoping applies to all vulpea-meta-* and vulpea-buffer-meta-* functions.

Tags

Per-Note Tag Operations

Get or modify tags for a specific note (works with both file-level and heading-level notes):

;; Get tags for a note
(vulpea-tags note-or-id)
;; => ("project" "active")

;; Add tags
(vulpea-tags-add note-or-id "new-tag")
(vulpea-tags-add note-or-id "tag1" "tag2" "tag3")

;; Remove tags
(vulpea-tags-remove note-or-id "old-tag")
(vulpea-tags-remove note-or-id "tag1" "tag2")

;; Set tags (replaces all existing tags)
(vulpea-tags-set note-or-id "only" "these" "tags")

For file-level notes (level = 0), these modify #+filetags. For heading-level notes (level > 0), they modify org heading tags.

Batch Tag Operations

Operate on multiple notes efficiently using vulpea-utils-process-notes:

;; Add a tag to multiple notes
(vulpea-tags-batch-add notes "archived")
;; => count of notes processed

;; Remove a tag from multiple notes
(vulpea-tags-batch-remove notes "active")
;; => count of notes processed

;; Rename a tag across all notes (implements #120)
(vulpea-tags-batch-rename "old-tag" "new-tag")
;; => count of notes modified

;; Interactive: M-x vulpea-tags-batch-rename
;; - Prompts for old tag (with completion from existing tags)
;; - Prompts for new tag name
;; - Shows count of modified notes

The rename function queries for all notes with the old tag, removes it, and adds the new tag. When called interactively via M-x vulpea-tags-batch-rename, it provides completion for existing tags.

Batch Metadata Operations

Operate on metadata across multiple notes:

;; Set a metadata property on multiple notes
(vulpea-meta-batch-set notes "status" "archived")
;; => count of notes processed

;; Set a list value
(vulpea-meta-batch-set notes "tags" '("a" "b" "c"))

;; Remove a metadata property from multiple notes
(vulpea-meta-batch-remove notes "deprecated-key")
;; => count of notes processed

Title Propagation

When you rename a note’s title, incoming links still show the old title as their description. Vulpea provides tools to propagate title changes to link descriptions and optionally rename the file.

vulpea-propagate-title-change

Interactive command to propagate a title change across the knowledge base:

;; Interactive usage
M-x vulpea-propagate-title-change

;; With prefix argument for dry-run preview
C-u M-x vulpea-propagate-title-change

;; Programmatic usage
(vulpea-propagate-title-change note-or-id)

The command:

  1. Identifies the note (from current buffer or prompts)
  2. Prompts for the old title if not detected by vulpea-title-change-detection-mode
  3. Offers to rename the file based on the new title slug
  4. Categorizes incoming links:
    • Exact matches: description equals old title or alias (case-insensitive) → can auto-update
    • Partial matches: description contains old title → manual review needed
  5. For exact matches, offers: [!] Update all, [r] Review individually, [s] Skip, [q] Quit
  6. For partial matches, offers to open files for manual editing

With C-u prefix (dry-run), shows a preview buffer without making changes.

vulpea-rename-file

Rename a note’s file based on a new title:

(vulpea-rename-file note-or-id "New Title")
;; => new file path

;; Renames: old_title.org → new_title.org (using slug)
;; Updates database
;; Signals error if target file exists

Only works for file-level notes (level = 0).

vulpea-title-change-detection-mode

Minor mode that detects title changes on save:

;; Enable for current buffer
(vulpea-title-change-detection-mode +1)

;; Enable for all org files
(add-hook 'org-mode-hook #'vulpea-title-change-detection-mode)

When enabled:

  • Captures the title before each save
  • After save, if title changed, shows a message: “Title changed. Run M-x vulpea-propagate-title-change to update references.”

This mode only notifies - it doesn’t automatically update anything.

Modifying Notes Programmatically

With Async Sync

For modifications where immediate database update isn’t needed:

(vulpea-utils-with-note note
  (vulpea-buffer-meta-set "status" "reviewed")
  (save-buffer))
;; Database updates asynchronously via file watcher

With Immediate Sync

When you need to query updated data right away:

(vulpea-utils-with-note-sync note
  (vulpea-buffer-meta-set "status" "done"))
;; Database is already updated here
(vulpea-note-meta-get (vulpea-db-get-by-id id) "status")
;; => "done"

Selection Interface

Select Note

;; Basic selection
(vulpea-select "Select note")
;; => selected vulpea-note or nil

;; With filter
(vulpea-select "Select project"
               :filter-fn (lambda (note)
                            (member "project" (vulpea-note-tags note))))

;; With initial input
(vulpea-select "Select" :initial-prompt "meeting")

;; With alias expansion (allows selecting by alias)
(vulpea-select "Select" :expand-aliases t)
;; When alias is selected, returned note has:
;; - title = the selected alias
;; - primary-title = the original title

Select from List

(vulpea-select-from "Choose" notes)
;; => selected note from provided list

;; With alias expansion
(vulpea-select-from "Choose" notes :expand-aliases t)
;; Each note with aliases appears multiple times in completion

Select Multiple from List

(vulpea-select-multiple-from "Choose" notes)
;; => list of selected notes (each note can only be selected once)

;; With options
(vulpea-select-multiple-from "Choose" notes
                             :require-match t
                             :expand-aliases t)

Customizing Selection Display

Control how notes are displayed in completion with vulpea-select-describe-fn:

;; Default: just the title
(setq vulpea-select-describe-fn #'vulpea-note-title)

;; Show outline path (parent headings) before title
;; Example: "Projects → Work → Task"
(setq vulpea-select-describe-fn #'vulpea-select-describe-outline)

;; Show file title and outline path before title
;; Example: "My Notes → Projects → Task"
(setq vulpea-select-describe-fn #'vulpea-select-describe-outline-full)

The outline variants are useful when using headings as notes (vulpea-db-index-heading-level) to disambiguate notes with identical titles.

Finding and Inserting Notes

vulpea-find

Interactive command for note selection and navigation:

;; Basic usage (interactive)
(vulpea-find)

;; With custom candidates source
(vulpea-find :candidates-fn (lambda (filter-fn)
                              (vulpea-db-query-by-tags-some '("project"))))

;; With filter
(vulpea-find :filter-fn (lambda (note)
                          (member "active" (vulpea-note-tags note))))

;; Open in other window
(vulpea-find :other-window t)

;; Require exact match (no creating new notes)
(vulpea-find :require-match t)

vulpea-insert

Interactive command for inserting a link to a note:

;; Basic usage (interactive)
(vulpea-insert)

;; With custom candidates source
(vulpea-insert :candidates-fn (lambda (filter-fn)
                                (vulpea-db-query-by-tags-some '("person"))))

;; With filter
(vulpea-insert :filter-fn (lambda (note)
                            (= 0 (vulpea-note-level note))))

;; With custom note creation
(vulpea-insert :create-fn (lambda (title props)
                            (vulpea-create title nil :tags '("new"))))

Both functions support :candidates-fn to customize the source of candidates, and :filter-fn to filter candidates. Global defaults can be configured via vulpea-find-default-candidates-source / vulpea-insert-default-candidates-source and vulpea-find-default-filter / vulpea-insert-default-filter.

Synchronization

Immediate Update

;; Update specific file (synchronous)
(vulpea-db-update-file "/path/to/note.org")

;; Update via sync system (async if autosync enabled)
(vulpea-db-sync-update-file "/path/to/note.org")

Batch Operations

;; Update directory
(vulpea-db-sync-update-directory "~/org/")

;; Full scan
(vulpea-db-sync-full-scan)

;; Force re-index
(vulpea-db-sync-full-scan 'force)

Utility Functions

;; Convert title to URL-friendly slug
(vulpea-title-to-slug "My Note Title")
;; => "my-note-title"

;; Make org link to note
(vulpea-utils-link-make-string note)
;; => "[[id:abc123][Note Title]]"

(vulpea-utils-link-make-string note "Custom Description")
;; => "[[id:abc123][Custom Description]]"

;; Expand note into multiple notes based on aliases
(vulpea-note-expand-aliases note)
;; Returns a list of notes:
;; - First element: original note (unchanged)
;; - Additional elements: copies with each alias as title
;;   and original title as primary-title
;;
;; Example with note having title "Full Name" and aliases ("Alias1" "Alias2"):
;; => (note-with-title="Full Name"
;;     note-with-title="Alias1" primary-title="Full Name"
;;     note-with-title="Alias2" primary-title="Full Name")

Database Access

For advanced use cases, direct database access:

;; Get database connection
(vulpea-db)

;; Run raw SQL
(emacsql (vulpea-db)
         [:select * :from notes :where (= id $s1)]
         "note-id")

→ See Plugin Guide for custom tables and extractors.

Quick Reference Tables

Core Functions

FunctionDescription
vulpea-findInteractive note selection and navigation
vulpea-find-backlinkFind notes linking to current note
vulpea-insertInsert link to a note
vulpea-createCreate file-level or heading-level note
vulpea-visitVisit note by ID or struct

Query Functions

FunctionDescription
vulpea-db-queryQuery with predicate
vulpea-db-get-by-idGet single note by ID
vulpea-db-query-by-idsGet multiple notes by IDs
vulpea-db-get-file-by-idGet file path for note ID
vulpea-db-search-by-titleSearch by title substring
vulpea-db-query-by-tags-someNotes with ANY tags
vulpea-db-query-by-tags-everyNotes with ALL tags
vulpea-db-query-by-tags-noneNotes WITHOUT tags
vulpea-db-query-by-links-someNotes linking to ANY IDs
vulpea-db-query-by-links-everyNotes linking to ALL IDs
vulpea-db-query-attachments-by-pathAttachment (dest . attach-dir) pairs
vulpea-db-query-by-metaNotes with metadata key=value
vulpea-db-query-by-meta-keyNotes having metadata key
vulpea-db-query-by-file-pathsNotes from specific files
vulpea-db-query-by-levelNotes at specific level
vulpea-db-query-tagsGet all unique tags
vulpea-db-query-dead-linksFind broken ID links
vulpea-db-query-orphan-notesNotes with no incoming ID links
vulpea-db-query-isolated-notesNotes with no connections at all
vulpea-db-query-title-collisionsNotes sharing the same title
vulpea-db-query-linksAll links as plists
vulpea-db-query-links-by-typeLinks filtered by type
vulpea-db-query-links-fromOutgoing links from a note
vulpea-db-query-links-toIncoming links (backlinks) to a note

Tag Functions

FunctionDescription
vulpea-tagsGet tags for a note
vulpea-tags-addAdd tags to a note
vulpea-tags-removeRemove tags from a note
vulpea-tags-setSet tags for a note (replace all)
vulpea-tags-batch-addAdd a tag to multiple notes
vulpea-tags-batch-removeRemove a tag from multiple notes
vulpea-tags-batch-renameRename a tag across all notes (interactive)
vulpea-meta-batch-setSet metadata property on notes
vulpea-meta-batch-removeRemove metadata property from notes

Buffer Functions

FunctionDescription
vulpea-buffer-title-getGet buffer title
vulpea-buffer-title-setSet buffer title
vulpea-buffer-tags-getGet buffer tags
vulpea-buffer-tags-setSet buffer tags
vulpea-buffer-tags-addAdd tag interactively
vulpea-buffer-tags-removeRemove tag interactively
vulpea-buffer-alias-getGet aliases
vulpea-buffer-alias-setSet aliases (replace all)
vulpea-buffer-alias-addAdd alias
vulpea-buffer-alias-removeRemove alias
vulpea-buffer-meta-setSet metadata value
vulpea-buffer-meta-set-batchSet multiple meta values
vulpea-buffer-meta-sortSort metadata keys

Sync Functions

FunctionDescription
vulpea-db-autosync-modeToggle auto-sync
vulpea-db-update-fileImmediate sync update
vulpea-db-sync-update-fileUpdate file (async if enabled)
vulpea-db-sync-update-directoryUpdate directory
vulpea-db-sync-full-scanScan all directories

Next Steps