mps-rs 1.7.5

MPS — plain-text personal productivity CLI (Rust)
Documentation

mps

Crates.io CI License: MIT

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

cargo install mps-rs

The installed binary is named mps.

From source

git clone https://github.com/mash-97/mps-rs
cd mps-rs
cargo install --path .

Quick start

mps                                          # open today's file in $EDITOR
mps list                                     # print today's elements
mps list --since monday                      # all elements since last Monday
mps append task "Fix the auth bug" --tags work,backend
mps append note "The edge case only appears under load"
mps append reminder "Team standup" --at 3pm
mps append log "Deep-work session" --start-time 09:00 --end-time 11:30
mps append character "Helped me think through the design" --name "Dr. Alice" --tags mentor
mps done task-1                              # mark task-1 done
mps edit note-1                              # rewrite note-1's body in $EDITOR
mps delete task-3 --yes                      # delete task-3 without confirmation
mps search "auth" --since "last week"
mps stats --since monday
mps export --format csv --since "20260501" > may.csv
mps autogit                                  # stage + commit + pull + push
mps notify --dry-run                         # preview due reminders + open tasks
mps daemon install                           # enable minutely background checker
mps meta show                                # inspect cross-device config layer
mps meta sync up                             # push YAML settings → .mps.meta
mps meta sync down                           # pull .mps.meta settings → YAML
mps serve                                    # start REST API on http://127.0.0.1:3000
mps serve --port 8080 --token mysecret       # custom port + Bearer auth

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                   # today
mps open yesterday
mps open last friday

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 list
mps list --type task --status open
mps list --since monday --refs
mps list --all --name "Dr. Alice"

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. ttask).

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 append task "Review the PR" --tags work,backend
mps append note "Cache invalidation approach"
mps append reminder "1:1 with manager" --at 2pm
mps append log "Debugging session" --start-time 14:00 --end-time 16:30
mps append character "Explained the system clearly" --name "Mahfuz Vai" --tags mentor,work
mps append task "Write tests" --status done --tags ci

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 edit task-1
mps edit note-2 --date yesterday
mps edit 20260601.1779000000.1   # epoch ref works too

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 delete task-1
mps delete note-3 --yes
mps delete task-2 --date yesterday

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 update task-1 --status done
mps update mps-1.2 --end-time 17:00
mps update task-3 --status done --date yesterday

mps done REFPATH [--date DATE]

Shorthand for mps update REFPATH --status done.

mps done task-1
mps done task-2 --date yesterday

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 search "auth"
mps search "design" --type character --name "Dr. Alice"
mps search "deploy" --since "last week" --type log

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 stats
mps stats --since monday
mps stats --all

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 tags
mps tags --all --type task --status open
mps tags --since monday --name "Dr. Alice"

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 export --format json
mps export --format csv --since "20260501" > may.csv
mps export --since monday --type log

mps config [show|edit]

View or edit configuration.

mps config         # same as mps config show
mps config show
mps config edit    # opens config file in $EDITOR

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.

mps notify --dry-run
mps notify --force              # re-fire even if already sent
mps notify --window 10          # 10-minute window

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)
mps daemon install
mps daemon status
mps daemon remove

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
mps meta            # same as mps meta show
mps meta edit
mps meta clear
mps meta sync up    # YAML → .mps.meta, then run: mps autogit
mps meta sync down  # .mps.meta → YAML (machine-specific paths unchanged)

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 git status
mps git auto          # add . + commit + pull + push
mps git autocommit    # add . + commit only
mps autogit           # same as mps git auto
mps cmd ls -la        # any shell command in storage dir

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
mps serve                          # http://127.0.0.1:3000, no auth
mps serve --port 8080              # custom port
mps serve --port 3000 --token abc  # require Authorization: Bearer abc

Endpoints

Method Path Description
GET /health {"status":"ok","version":"1.7.0"}
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.

# Quick examples
curl http://localhost:3000/health
curl http://localhost:3000/elements?all=true
curl -X POST http://localhost:3000/elements \
     -H "Content-Type: application/json" \
     -d '{"type":"task","body":"ship it","tags":["work"]}'
curl -X PATCH http://localhost:3000/elements/20260525.1 \
     -H "Content-Type: application/json" \
     -d '{"status":"done"}'
curl "http://localhost:3000/export?format=csv&since=20260501" > may.csv

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
mondaysunday 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 (aappend, ttask)
  • mps + n "Interesting observation" → appends a note
  • mps s "auth" → searches for "auth"

Override the config path:

mps --config-path /path/to/other.yaml list
# or
MPS_CONFIG=/path/to/other.yaml mps list

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):

{
  "version": 1,
  "config": {
    "type_aliases":    { "t": "task", "c": "character" },
    "command_aliases": { "a": "append", "+": "append" },
    "default_command": "list",
    "custom_tags":     ["work", "personal", "urgent", "health"],
    "notify": {
      "enabled":               true,
      "window_minutes":        5,
      "task_notify_at":        "9am",
      "notify_open_tasks":     true,
      "open_task_tags":        [],
      "task_cooldown_minutes": 60,
      "overdue_days":          7
    }
  }
}

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 (for git / autogit commands)
  • $EDITOR or $VISUAL (for open / config edit / meta edit; falls back to vim)
  • notify-send (for mps 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.1 (2026-05-25)

  • Browser UI served at GET / when mps serve is 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: n new element, / search, 1–5 switch tabs, Esc close modal
  • GET /elements/:ref — new endpoint: fetch a single element by epoch or human ref
  • PATCH /elements/:ref now accepts body field 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
  • ServeConfig in ~/.mps_config.yaml (port, host, token); overridable via --port/--host/--token flags
  • src/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 via reqwest
  • 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.meta in one command; run mps autogit to propagate to other devices
  • mps meta sync down — pull settings from .mps.meta into your local YAML config file; machine-specific paths (storage_dir, mps_dir, log_file) are never modified
  • Both sync subcommands respect the global --config-path flag
  • Config::save() — atomic YAML write (tmp + rename) used by sync 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 shorter window_minutes (default 5 min) — reminders were re-firing too frequently
  • Fix: merge_meta() notify merge is now field-by-field — a task_notify_at in .mps.meta no longer silently overwrites window_minutes set in ~/.mps_config.yaml
  • Fix: mps edit now presents the element body to $EDITOR without leading indentation (dedented), then re-indents correctly on save
  • Fix: mps meta edit validates the saved JSON immediately after the editor closes and warns if it is broken
  • Fix: mps notify task briefing body is capped at 10 lines with an … and N more suffix to avoid overflowing the notification popup
  • Tests: 559 tests total (up from 478) — new coverage across dedent, extract/replace body, find_element_span with 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 unchanged
  • mps 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 via notify-send
  • mps daemon install|remove|status|run — systemd user timer for minutely checks
  • mps meta [show|edit|clear] — inspect and edit the .mps.meta cross-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