mps
A plain-text personal productivity CLI. Append journal entries, tasks, reminders, logs, and notes to daily .mps files; search, list, and export them; sync via git. Single binary, no runtime dependencies.
Rust rewrite of mps (Ruby). Fully backward-compatible with all Ruby-generated files and config.
Install
The installed binary is named mps.
From source
Quick start
Element types
| Type | Description |
|---|---|
task |
Something to do, with open/done status |
note |
Free-form observation or thought |
reminder |
Time-anchored alert |
log |
Timed work block (start/end time → duration) |
character |
Running monologue about a person |
Commands
mps [open] [DATE]
Open the .mps file for DATE in $EDITOR (falls back to vim). Creates the file if absent.
mps list [DATE]
Print elements for DATE as an indented tree.
| Flag | Short | Description |
|---|---|---|
--type TYPE |
-t |
Filter by type: task, note, log, reminder, character |
--tag TAG |
-g |
Filter by tag name |
--status STATUS |
-s |
Filter tasks by open or done (hides all non-task elements) |
--name NAME |
-n |
Filter character entries by person name |
--since DATE |
-S |
Show elements from DATE up to the target date |
--refs |
-r |
Show human-readable ref column (task-1, mps-1.2, …) |
--all |
-a |
List across the entire archive, not just one date |
mps append TYPE BODY [FLAGS]
Append one element to today's file without opening an editor. TYPE is resolved through type_aliases from config (e.g. t → task).
| Flag | Description |
|---|---|
--tags t1,t2 |
Comma-separated tags |
--status open|done |
Task status (default: open) |
--at TIME |
Time for reminders (5pm, 10:30, …) |
--start-time HH:MM |
Start time for logs |
--end-time HH:MM |
End time for logs |
--name NAME |
-n Person name for character entries |
mps edit REFPATH [--date DATE]
Open an element's body text in $EDITOR. The body is written to a temp file; on save the original file is updated atomically. No-op if the content is unchanged.
mps delete REFPATH [FLAGS]
Remove an element entirely from its file. Prompts for confirmation unless --yes is given.
| Flag | Short | Description |
|---|---|---|
--yes |
-y |
Skip confirmation prompt |
--date DATE |
-d |
Date context for human refs (default: today) |
mps update REFPATH [FLAGS]
Update one element's attributes in-place. REFPATH is a human ref (task-1, mps-1.2) or an epoch ref (20260428.1732500287.1).
| Flag | Description |
|---|---|
--status open|done |
Set task status |
--start-time HH:MM |
Set log start time |
--end-time HH:MM |
Set log end time |
--at TIME |
Set reminder time |
--date DATE |
-d Date context for human refs (default: today) |
mps done REFPATH [--date DATE]
Shorthand for mps update REFPATH --status done.
mps search QUERY [FLAGS]
Full-text search across all .mps files. Returns matching elements with date and ref.
| Flag | Short | Description |
|---|---|---|
--type TYPE |
-t |
Filter by element type |
--tag TAG |
-g |
Filter by tag |
--name NAME |
-n |
Filter character entries by person name |
--since DATE |
-S |
Search from DATE onward |
mps stats [DATE] [FLAGS]
Show element counts and total log durations.
| Flag | Short | Description |
|---|---|---|
--since DATE |
-S |
Stats from DATE up to target date |
--all |
-a |
Stats across the entire archive |
mps tags [DATE] [FLAGS]
Show tag usage as a frequency bar chart.
| Flag | Short | Description |
|---|---|---|
--type TYPE |
-t |
Count tags for this element type only |
--status STATUS |
-s |
Restrict to tasks with this status |
--name NAME |
-n |
Restrict to character entries for this person |
--since DATE |
-S |
Tags from DATE up to target date |
--all |
-a |
Count across the entire archive |
mps export [DATE] [FLAGS]
Export elements to stdout as JSON or CSV.
| Flag | Short | Description |
|---|---|---|
--format json|csv |
-f |
Output format (default: json) |
--type TYPE |
-t |
Filter by element type |
--since DATE |
-S |
Export from DATE up to target date |
CSV columns: date, ref, type, tags, body, status, at, start, end, name
mps config [show|edit]
View or edit configuration.
mps notify [FLAGS]
Check for due reminders and open tasks, then send desktop notifications via notify-send.
| Flag | Description |
|---|---|
--dry-run |
Print what would be sent without actually sending |
--window MINS |
Override the due-now window in minutes (default: from config) |
--force |
Fire even if already notified within the window |
Reminders fire when the current time falls within window_minutes of the reminder's --at time. Each reminder is deduplicated by its ref so it won't re-fire until the cooldown has elapsed.
Open-task briefing fires once per day at task_notify_at time (e.g. "9am"), listing all open tasks from today plus up to overdue_days past days.
Notification settings live in ~/.mps_config.yaml under the notify: key or in .mps.meta for cross-device sync (see Meta config).
mps daemon SUBCOMMAND
Manage a systemd user timer that calls mps daemon run (= mps notify) once per minute.
| Subcommand | Description |
|---|---|
install |
Write unit files, enable and start the timer |
remove |
Stop and disable the timer, remove unit files |
status |
Pass-through to systemctl --user status mps-notify.timer |
run |
Run one notify tick (invoked by systemd; also safe to call manually) |
Unit files are written to ~/.config/systemd/user/. .mps.local (notification history) is automatically added to .gitignore on install so it stays off git.
mps meta [SUBCOMMAND]
Inspect or edit the .mps.meta sidecar config file.
| Subcommand | Description |
|---|---|
show (default) |
Pretty-print .mps.meta and .mps.local |
edit |
Open .mps.meta in $EDITOR |
clear |
Delete .mps.local (notification history + cache) |
sync up |
Push machine-agnostic settings from YAML config → .mps.meta |
sync down |
Pull settings from .mps.meta → YAML config file |
sync up reads your current ~/.mps_config.yaml and writes all machine-agnostic
fields (type_aliases, command_aliases, default_command, custom_tags, notify)
into .mps.meta. Run mps autogit afterwards to push the update to other devices.
sync down reads .mps.meta and applies its fields into your local YAML config file.
Machine-specific paths (storage_dir, mps_dir, log_file) are never touched.
Both subcommands respect --config-path.
mps git ARGS / mps autogit / mps cmd ARGS
Run git or shell commands inside the storage directory.
mps serve [FLAGS]
Start an Axum HTTP server that exposes all mps operations as a JSON REST API. Designed for browser tooling, mobile clients, and AI integrations.
| Flag | Short | Description |
|---|---|---|
--port PORT |
-p |
TCP port to listen on (default: 3000) |
--host HOST |
Host address to bind (default: 127.0.0.1) |
|
--token TOKEN |
-t |
Bearer token for API auth; empty string = no auth |
Endpoints
| Method | Path | Description |
|---|---|---|
GET |
/health |
{"status":"ok","version":"1.7.6"} |
GET |
/elements |
List elements (?date, ?type, ?tag, ?since, ?all=true) |
POST |
/elements |
Append element; returns {"ref":"...","human_ref":"..."} (201) |
PATCH |
/elements/:ref |
Update attributes in-place; returns {"updated":true} |
DELETE |
/elements/:ref |
Delete element; returns {"deleted":true} |
GET |
/search |
Full-text search (?q, ?type, ?tag, ?since) |
GET |
/stats |
Element counts + log durations (?date, ?since, ?all=true) |
GET |
/export |
Export as JSON or CSV (?format=json|csv, ?date, ?since, ?type) |
GET |
/tags |
Tag frequency map (?date, ?type, ?all=true) |
All responses are JSON. CORS is permissive (any origin). Auth is skipped when token is empty.
Browser UI
Opening http://localhost:3000/ in any browser loads a fully self-contained single-page app — no CDN, no external dependencies, works offline against the local server.
| Feature | Details |
|---|---|
| Today tab | Live stats pills, append form (all 5 types with per-type fields), type/tag filter, element count indicator |
| Search tab | Full-text + type/tag/since filters, auto-focused on tab switch |
| Archive tab | Date picker with ‹ › navigation |
| Tags tab | Frequency bar chart, type filter, all-dates toggle |
| Stats tab | Big-number cards, task completion bar, dates-covered chips |
| Edit modal | Inline body + attribute editing, empty-body validation |
| Font size | A− / A / A+ controls, 3 levels, persisted in localStorage |
| Font family | Sans / Serif / Mono button group, persisted in localStorage |
| Theme | 🌙 / ☀️ toggle, dark (default) and light, persisted in localStorage |
| Keyboard | 1–5 switch tabs, n new entry, / search, Ctrl+Enter submit form, Esc close |
# Quick examples
Server settings can also live in ~/.mps_config.yaml under the serve: key — see Configuration.
mps version
Print the version string.
File format
Files are named YYYYMMDD.<epoch>.mps (or YYYYMMDD.mps for files without an epoch). The format is identical to the Ruby gem — no migration needed.
@task[work, release, status: done]{
Ship the API refactor
}
@note{
The auth token expiry edge case only appears under concurrent load
}
@reminder[at: 3pm]{
Team standup
}
@log[work, start: 09:00, end: 11:30]{
Debugging the auth flow
}
@character[name: Dr. Alice, mentor, trusted]{
Explained the layered caching approach in detail.
Would consult again for architecture decisions.
}
@mps[sprint-42]{
@task[backend]{
Nested task inside a sprint block
}
@note{
Retrospective note
}
}
Brackets are optional — @task{ body } is valid. Tags and named attributes share the bracket: [tag1, tag2, status: done, at: 5pm].
Date formats
Accepted everywhere a DATE is expected:
| Input | Meaning |
|---|---|
today |
Today |
yesterday |
Yesterday |
monday … sunday |
Most recent occurrence of that weekday |
last friday |
The Friday before the most recent one |
3 days ago |
3 days before today |
last week |
7 days ago |
20260421 |
Explicit YYYYMMDD |
2026-04-21 |
Explicit YYYY-MM-DD |
Configuration
Config file: ~/.mps_config.yaml. Created automatically on first run. The same file written by the Ruby gem is accepted without changes (Ruby symbol-key YAML like :storage_dir: is handled transparently).
mps_dir: /home/you/.mps
storage_dir: /home/you/.mps/mps
log_file: /home/you/.mps/mps.log
git_remote: origin
git_branch: master
default_command: list # command run by bare `mps` invocation (open or list)
# Short-hand element type aliases (also accepts legacy key: aliases)
type_aliases:
t: task
n: note
r: reminder
l: log
c: character
# Short-hand command aliases
command_aliases:
a: append
"+": append
s: search
l: list
# Canonical tag list synced across devices via .mps.meta
custom_tags:
- work
- personal
- urgent
# Desktop notification settings
notify:
enabled: true
window_minutes: 5 # minutes either side of a reminder time counts as "due"
task_notify_at: "9am" # morning briefing time; omit to disable open-task briefing
notify_open_tasks: true
open_task_tags: # if non-empty, only tasks with these tags are included
task_cooldown_minutes: 60 # min gap between repeat notifications for the same reminder
overdue_days: 7 # how many past days to scan for overdue open tasks
# HTTP API server settings (machine-specific, not synced via .mps.meta)
serve:
port: 3000
host: 127.0.0.1
token: "" # empty = no auth; set to require Bearer token
With the above config:
mps a t "Fix the bug" --tags work→ appends a task (a→append,t→task)mps + n "Interesting observation"→ appends a notemps s "auth"→ searches for "auth"
Override the config path:
# or
MPS_CONFIG=/path/to/other.yaml
Meta config
.mps.meta is a JSON sidecar file at ~/.mps/mps/.mps.meta that is git-tracked and synced across all your devices via mps autogit. It acts as a second config layer — machine-agnostic settings you want consistent everywhere.
Fields supported (union-merged with ~/.mps_config.yaml at startup; meta wins for scalars, maps are unioned with YAML taking priority on key conflicts):
Edit it directly with mps meta edit. Inspect the current state with mps meta show.
Use mps meta sync up to push your YAML settings into the file, and mps meta sync down
to pull the shared settings back into your local YAML.
.mps.local (also in ~/.mps/mps/) holds per-device transient state (notification history, cache). It is gitignored and never committed.
Architecture
src/
main.rs Entry point — alias pre-processing, clap dispatch
cli.rs Cli + Commands (#[derive(Parser)])
config.rs Config struct; YAML load/init; Ruby symbol-key normalization; ServeConfig
meta.rs MetaShared (.mps.meta) + MetaLocal (.mps.local) structs
time_parse.rs parse_time() — "5pm", "9:30am", "17:00", "noon"
constants.rs Filename regexes, new_file_name()
date_parse.rs parse_date() — natural-language + absolute formats
error.rs MpsError (thiserror)
parser.rs Position-based stack parser; mirrors Ruby Engines::Parser
ref_resolver.rs Bidirectional epoch ↔ human ref (task-1, mps-1.2)
store.rs Store — all filesystem I/O; append, parse, search, rewrite
api/ JSON request/response types + element_to_json, compute_stats helpers
elements/ Element enum + per-type Data structs
commands/ One module per command + shared display helpers
serve.rs — Axum router, Bearer auth middleware, all HTTP handlers
Two sidecar files live in storage_dir (~/.mps/mps/):
| File | Tracked | Purpose |
|---|---|---|
.mps.meta |
Git-tracked | Second config layer — aliases, notify settings, custom tags |
.mps.local |
Gitignored | Per-device state — notification history, cache |
Requirements
- Rust 1.70+
git(forgit/autogitcommands)$EDITORor$VISUAL(foropen/config edit/meta edit; falls back tovim)notify-send(formps notify/mps daemon— Linux desktop notifications; optional)
Contributing
Bug reports and pull requests are welcome. See CONTRIBUTING.md for development setup, test instructions, and the PR checklist. If you have an idea or found a bug, please open an issue first so we can discuss before you start coding.
Changelog
v1.7.6 (2026-05-25)
- Fix: Editing a task and setting status to
donewithout changing the body text incorrectly returned404 "ref not found"—replace_element_bodyreturnsOk(false)for both "not found" and "body unchanged", and the PATCH handler was treating both as 404 before reaching therewrite_elementcall for attr updates; fixed by only aborting with 404 when body update returns false and there are no attrs to update - Fix (defense-in-depth):
saveEdit()in the browser UI now only includesbodyin the PATCH payload if the value actually changed from what was fetched, avoiding the ambiguity entirely - Total tests: 818 (unchanged)
v1.7.5 (2026-05-25)
- Fix (critical):
initwas not exported in the public API —App.init()threwTypeErroron load, silently breaking the entire browser UI (theme/font restore, archive date init, tab loading all failed) - Fix: light-mode hover on element items was invisible (
--surface == --card == #fff); now uses a neutral tint - Fix: font-family
<select>replaced with a consistentSans / Serif / Monobutton group matching the font-size controls; removes unwanted focus box-shadow - Fix: font-family active button state now correctly restored from
localStorageon load - UX: Search tab auto-focuses the query input when switched to
- UX:
Ctrl+Enter(or⌘+Enter) in the append-form textarea submits the form - UX: Edit modal validates non-empty body before patching; shows an error toast if blank
- UX: Today filter bar shows element count (
3 of 7when filtered,7 elementsotherwise) - UX: Raw Axum 422 deserialize errors are caught and replaced with
"Invalid request: check required fields." - Total tests: 818 (unchanged)
v1.7.4 (2026-05-25)
- Font family selector added to the topbar:
Sans(system default),Serif(Georgia),Mono(monospace) - Uses
--font-familyCSS variable on:root; preference persisted inlocalStorageand restored on load
v1.7.3 (2026-05-25)
- Comprehensive UI revision: CSS variable–based font scaling, custom scrollbar, smooth focus rings,
fadeDownanimation on append form, blur backdrop on modal, sticky modal header/footer - Font size controls (
A−/A/A+) in topbar — 3 levels (text-sm, default,text-lg), persisted inlocalStorage; all font sizes use--fs-*CSS variables so the whole UI scales together - Colored left-border accent stripe on element items (by type), visible on hover
- Stat pills redesigned with vertical card layout and sub-label line
- Stats page: larger number cards (
2.4em), task completion progress bar with open/done breakdown - Today tab shows day-of-week in date label
- Keyboard hints footer at page bottom (
1–5,n,/,Esc) - Better empty states with icons; improved tag chart bar height
v1.7.2 (2026-05-25)
- Light/dark theme toggle (🌙/☀️) in topbar, persisted in
localStorageand restored on page load - CSS uses
html.light {}class with--bg,--surface,--card,--text,--muted,--accent, and all type-color variables overridden for light palette
v1.7.1 (2026-05-25)
- Browser UI served at
GET /whenmps serveis running — no external dependencies, fully self-contained dark-theme SPA - UI features: Today dashboard with live stats, append form (all 5 element types), per-type field prompts, mark-done, delete, inline body+attribute editing, full-text search with filters, archive browser (date navigation), tag frequency bar chart, stats dashboard with task completion bar
- Keyboard shortcuts:
nnew element,/search,1–5switch tabs,Escclose modal GET /elements/:ref— new endpoint: fetch a single element by epoch or human refPATCH /elements/:refnow acceptsbodyfield for in-place body text editing- 7 new tests (3 UI, 2 GET single element, 2 PATCH body) — total: 818 tests
v1.7.0 (2026-05-25)
mps serve— Axum HTTP server exposing all mps operations as a JSON REST API- 9 endpoints:
/health,/elements(GET/POST),/elements/:ref(PATCH/DELETE),/search,/stats,/export,/tags - Bearer token authentication (optional; empty token = no auth)
- CORS permissive for browser-based tooling, Obsidian plugins, and AI integrations
ServeConfigin~/.mps_config.yaml(port,host,token); overridable via--port/--host/--tokenflagssrc/api/module with shared JSON types (ElementJson,AppendRequest,UpdateRequest,StatsJson) and conversion helpers- 50 new tests: 47 in-process via
tower::ServiceExt::oneshot+ 3 real-TCP smoke tests viareqwest - Total test suite: 811 tests
v1.6.2 (2026-05-25)
mps meta sync up— push machine-agnostic settings from your YAML config into.mps.metain one command; runmps autogitto propagate to other devicesmps meta sync down— pull settings from.mps.metainto your local YAML config file; machine-specific paths (storage_dir,mps_dir,log_file) are never modified- Both
syncsubcommands respect the global--config-pathflag Config::save()— atomic YAML write (tmp + rename) used bysync down
v1.6.1 (2026-05-24)
Bug fixes and test quality improvements.
- Fix: reminder deduplication now respects
task_cooldown_minutes(default 60 min) instead of the shorterwindow_minutes(default 5 min) — reminders were re-firing too frequently - Fix:
merge_meta()notify merge is now field-by-field — atask_notify_atin.mps.metano longer silently overwriteswindow_minutesset in~/.mps_config.yaml - Fix:
mps editnow presents the element body to$EDITORwithout leading indentation (dedented), then re-indents correctly on save - Fix:
mps meta editvalidates the saved JSON immediately after the editor closes and warns if it is broken - Fix:
mps notifytask briefing body is capped at 10 lines with an… and N moresuffix to avoid overflowing the notification popup - Tests: 559 tests total (up from 478) — new coverage across
dedent,extract/replace body,find_element_spanwith nested elements, atomic save cleanup, MetaShared/MetaLocal JSON corruption fallback, notify cooldown, notify merge field semantics, time parser edge cases, and delete/replace correctness across multiple same-type elements
v1.6.0 (2026-05-24)
mps edit REFPATH— open an element's body in$EDITOR; writes back atomically, no-op if unchangedmps delete REFPATH [--yes]— remove an element from its file with confirmation prompt
v1.5.0 (2026-05-06)
mps notify [--dry-run] [--window MINS] [--force]— desktop notifications vianotify-sendmps daemon install|remove|status|run— systemd user timer for minutely checksmps meta [show|edit|clear]— inspect and edit the.mps.metacross-device config layer.mps.meta— git-tracked JSON sidecar: aliases, notify schedule, custom tags synced across devices.mps.local— gitignored per-device state: notification history, tag cache
License
MIT