An open API service indexing awesome lists of open source software.

https://github.com/cuttlefisch/or-east

Org Roam Extended Attribute Stat Tracking, OR-EAST
https://github.com/cuttlefisch/or-east

emacs-lisp emacs-mode

Last synced: about 1 month ago
JSON representation

Org Roam Extended Attribute Stat Tracking, OR-EAST

Awesome Lists containing this project

README

          

#+TITLE: or-east

#+HTML: CI
#+HTML: License: GPL v3
#+HTML: Emacs 28.1+

*Org Roam Extended Attribute Stat Tracking*

=or-east= is an Emacs minor mode that automatically tracks usage statistics on
[[https://github.com/org-roam/org-roam][org-roam]] nodes. It records timestamps in each node's property drawer whenever
you access, modify, or link to a node — no manual bookkeeping required.

* Tracked Properties

| Property | Updated when | Hook |
|-----------------+------------------------------------------------------+-------------------------------------|
| =last-accessed= | Node is opened via =org-roam-find-file= | =org-roam-find-file-hook= |
| =last-modified= | Body content changes (detected via =buffer-hash=) | =after-save-hook= (buffer-local) |
| =last-linked= | Another node inserts a link to this node | =org-roam-post-node-insert-hook= |

* How it works

When =or-east-mode= is enabled, it installs three hooks:

1. *Access tracking*: =org-roam-find-file-hook= runs
=or-east-node-update-access-time-by-id=, which sets =last-accessed= to the
current timestamp whenever you open a node.

2. *Modification tracking*: The same hook also runs
=or-east--setup-modified-tracking=, which adds =or-east-node-update-stats= to
the buffer-local =after-save-hook=. On each save, or-east computes a
=buffer-hash= of the file content from the =#+title= keyword onward and
compares it to a stored =hash= property. If they differ, both =hash= and
=last-modified= are updated. This means property-drawer-only changes (like
the timestamps themselves) do not trigger a modification.

3. *Link tracking*: =org-roam-post-node-insert-hook= runs
=or-east-node-update-link-time-by-id= on the /target/ node whenever a link
is inserted from another node.

All saves performed by or-east use an inhibit flag to prevent recursive
=after-save-hook= triggers.

* Installation

** straight.el / Doom Emacs

#+begin_src elisp
;; In packages.el (Doom)
(package! or-east :recipe (:host github :repo "cuttlefisch/or-east"))

;; In config.el
(use-package! or-east-mode
:hook (org-roam-find-file . or-east-mode))
#+end_src

** use-package + straight

#+begin_src elisp
(use-package or-east-mode
:straight (:host github :repo "cuttlefisch/or-east")
:hook (org-roam-find-file . or-east-mode))
#+end_src

** Manual

Clone the repository, add it to your =load-path=, and require it:

#+begin_src elisp
(add-to-list 'load-path "/path/to/or-east")
(require 'or-east-mode)
(add-hook 'org-roam-find-file-hook #'or-east-mode)
#+end_src

* Usage

Once =or-east-mode= is active in an org-roam buffer, the three properties update
transparently during normal use. You can also call functions directly:

- =or-east-enable= / =or-east-disable= — activate or deactivate the mode
- =or-east-node-update-stats= — manually trigger a stat update (=M-x=)
- =or-east-normalize-timestamps= — batch-rewrite all node timestamps to the current format

* Configuration

| Variable | Default | Description |
|-----------------------------------------+---------+------------------------------------|
| =or-east-node-stat-format-time-string= | ="%Y-%m-%d"= | Format string for timestamps (passed to =format-time-string=) |
| =or-east-activity-weights= | see below | Weights for each tracked property in the activity score |
| =or-east-activity-decay-rate= | =0.01= | How quickly scores decay with age (higher = faster decay) |

The default format ="%Y-%m-%d"= produces ISO 8601 dates like =2026-03-21=.
Change it to suit your preference:

#+begin_src elisp
(setq or-east-node-stat-format-time-string "%Y-%m-%d %H:%M")
#+end_src

* Activity-based search prioritization

=or-east= can sort =org-roam-node-find= results by activity, so recently
modified/accessed/linked nodes appear first. Each tracked property
contributes a weighted, time-decayed score:

#+begin_src elisp
;; Default weights — modify contributes most, linking least
(setq or-east-activity-weights
'((last-accessed . 1.0)
(last-modified . 2.0)
(last-linked . 0.5)))

;; Decay rate controls how fast old activity fades
;; 0.01 = gradual (50% at ~100 days), 0.05 = aggressive (50% at ~20 days)
(setq or-east-activity-decay-rate 0.01)
#+end_src

To enable activity sorting as the default for =org-roam-node-find=:

#+begin_src elisp
(setq org-roam-node-default-sort 'activity)
#+end_src

This works because =or-east= registers =org-roam-node-read-sort-by-activity=
following org-roam's naming convention. You can also pass the sort
function directly:

#+begin_src elisp
(org-roam-node-find nil nil #'or-east-node-sort-by-activity)
#+end_src

** Freshness-weighted search

Use activity sorting as a tiebreaker so that equally-matched search results
are ranked by recency:

#+begin_src elisp
;; Make org-roam-node-find default to activity-based ordering
(setq org-roam-node-default-sort 'activity)
#+end_src

With this setting, when you type a query that matches several nodes equally
well, the ones you've touched most recently float to the top.

** Custom weight profiles

Tailor the weights to your workflow. For example, a research-heavy config
that prioritizes discovery through links:

#+begin_src elisp
;; Research mode: heavily weight links for discovery
(setq or-east-activity-weights
'((last-accessed . 0.5)
(last-modified . 1.0)
(last-linked . 3.0)))
#+end_src

Or a daily-driver config that favors nodes you open frequently:

#+begin_src elisp
;; Daily driver: favor recently accessed nodes
(setq or-east-activity-weights
'((last-accessed . 3.0)
(last-modified . 1.0)
(last-linked . 0.5)))
#+end_src

** Agenda/dashboard integration

Query nodes by activity score for a "recently active nodes" dashboard:

#+begin_src elisp
(defun my/or-east-top-nodes (&optional n)
"Return the top N most active org-roam nodes (default 10)."
(let* ((n (or n 10))
(nodes (org-roam-node-list))
(scored (sort nodes
(lambda (a b)
(> (or-east-node-activity-score a)
(or-east-node-activity-score b))))))
(seq-take scored n)))

;; Example: print titles of your 5 most active nodes
(dolist (node (my/or-east-top-nodes 5))
(message "%s (score: %.2f)"
(org-roam-node-title node)
(or-east-node-activity-score node)))
#+end_src

* Development

#+begin_src shell
cask install
make test
#+end_src

See [[file:CONTRIBUTING.org][CONTRIBUTING.org]] for full development guidelines.

* Changelog

See [[file:CHANGELOG.org][CHANGELOG.org]] for a list of notable changes.

* License

[[file:LICENSE][GNU General Public License v3.0]]