mps
A plain-text personal productivity CLI. Journal your day as tasks, notes, reminders, logs, and character entries in simple .mps files. Search, list, tag, and export them. Sync across devices via git. Chat with your journal through any local or remote LLM. Single binary, no runtime dependencies.
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 init
Interactive first-run setup wizard. Prompts for your journal directory, git settings, default command, and LLM chat URL/model. Creates ~/.mps_config.yaml and all required directories.
If a config already exists you are prompted to confirm before overwriting. Typing y, Y, yes, or YES proceeds; anything else aborts.
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 [SUBCOMMAND]
View, edit, or validate configuration.
| Subcommand | Description |
|---|---|
show (default) |
Print all active config values |
edit |
Open config file in $EDITOR |
init |
Write every config key explicitly to the YAML file (idempotent, no prompts) |
check |
Validate config health — paths exist, aliases are valid, chat URL reachable |
mps config check prints a ✓/✗ line per item and exits non-zero if any check fails, making it suitable for scripting:
Config health check
/home/you/.mps_config.yaml
✓ mps_dir : /home/you/.mps
✓ storage_dir : /home/you/.mps/mps
✓ log_file : /home/you/.mps/mps.log
✓ type_aliases : t→task, n→note
✓ command_aliases: a→append, l→list
✓ chat.url : http://localhost:11434 (reachable)
✓ all checks passed
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,
and non-sensitive chat fields: url, model, context_days, stream) 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) and chat.api_key are never touched.
Both subcommands respect --config-path.
mps chat [FLAGS]
Interactive REPL that loads recent journal entries into an LLM's context window and lets you ask natural-language questions about them. Works with any OpenAI-compatible endpoint — Ollama, llama.cpp, OpenAI API, or any compatible provider.
| Flag | Short | Description |
|---|---|---|
--url URL |
LLM base URL (default: auto-detect :11434 then :8080) |
|
--model MODEL |
Model name (default: llama3.2) |
|
--api-key KEY |
Bearer token for keyed APIs (empty = no auth) | |
--context-days N |
Days of history to load (default: 7) |
|
--since DATE |
Load journal from DATE onward (overrides --context-days) |
|
--all |
Load entire journal history | |
--stream BOOL |
Stream tokens as they arrive (default: true) |
|
--session-name NAME |
-s |
Named session: resume if it exists, start new if not |
--new |
Force a fresh session even if a named one already exists | |
--list-sessions |
Print all saved sessions and exit |
How it works
Each session builds a system prompt from scratch using your journal entries — formatted with date headers and element badges ([task], [note], [log], …). The prompt is rebuilt on every load so new entries appear automatically, even when resuming an old session.
Sessions are saved to ~/.mps/sessions/NAME.json after each assistant response and are git-tracked (so they sync across devices with mps autogit). api_key is never written to session files or to .mps.meta.
REPL commands
| Command | Description |
|---|---|
:help |
List available commands |
:context |
Print the current system prompt (your journal entries) |
:reload |
Rebuild context from disk (picks up entries added since session started) |
:session |
Show session name, message count, last saved |
:sessions |
List all saved sessions |
:save [NAME] |
Save current session (to current name or NAME) |
:save-as NAME |
Save a copy under a new name and switch to it |
:load NAME |
Load a saved session (replaces current message history) |
:clear |
Clear conversation history, keep session name and context |
:quit / :exit |
Exit (auto-saves if session is named) |
LLM backend auto-detection
If --url is not set, mps probes http://localhost:11434 (Ollama) then http://localhost:8080 (llama.cpp). Set chat.url in your config or .mps.meta to pin a specific endpoint.
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.8.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.
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 |
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 older files without an epoch suffix).
@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, or interactively via mps init. Run mps config init at any time to ensure all keys are explicitly present.
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
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
# LLM chat settings
chat:
url: null # null = auto-detect :11434 (Ollama) then :8080 (llama.cpp)
model: llama3.2
context_days: 7
stream: true
api_key: "" # Bearer token for keyed APIs (OpenAI, etc.) — never synced
sessions_dir: null # null = ~/.mps/sessions/
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
Note: Legacy symbol-key YAML configs (
:storage_dir: /path) are normalised transparently on load.
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):
chat.api_key and chat.sessions_dir are never written to .mps.meta — they stay local.
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 + ChatConfig + ServeConfig; YAML load/save/init; merge_meta()
meta.rs MetaShared (.mps.meta) + MetaLocal (.mps.local) + ChatMetaConfig
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
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 + helpers for the REST API
elements/ Element enum + per-type Data structs
llm/
mod.rs LlmClient — OpenAI-compatible streaming SSE client; auto_detect()
context.rs build_system_prompt() — formats journal entries for LLM context
session.rs ChatSession — save/load/list named sessions (JSON, atomic write)
commands/
chat.rs mps chat REPL — session lifecycle, rustyline loop, REPL commands
config_cmd.rs mps config show/edit/init/check (inc. tcp_reachable health check)
init_cmd.rs mps init interactive wizard
serve.rs Axum router, Bearer auth middleware, all HTTP handlers
… one module per remaining command + shared display helpers
Two sidecar files live in storage_dir (~/.mps/mps/):
| File | Tracked | Purpose |
|---|---|---|
.mps.meta |
Git-tracked | Second config layer — aliases, notify settings, chat settings, custom tags |
.mps.local |
Gitignored | Per-device state — notification history, cache |
Chat sessions live in ~/.mps/sessions/ (git-tracked; path configurable via chat.sessions_dir).
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)- LLM for
mps chat: Ollama (local, recommended), llama.cpp server, or any OpenAI-compatible API endpoint (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.8.0 (2026-05-27)
mps chat— interactive LLM chat REPL using your journal as context- Loads recent entries into a system prompt (context stuffing, rebuilt fresh every session load)
- OpenAI-compatible streaming API: works with Ollama, llama.cpp, OpenAI, and any compatible endpoint
- Auto-detects local Ollama (
:11434) or llama.cpp (:8080) when no URL is configured - Named sessions persisted to
~/.mps/sessions/NAME.json, git-tracked, auto-saved after each reply api_keyis local-only — never written to session files or.mps.meta- REPL commands:
:context,:reload,:session,:sessions,:save,:save-as,:load,:clear,:help,:quit - CLI flags:
--url,--model,--api-key,--context-days,--since,--all,--stream,--session-name/-s,--new,--list-sessions
mps init— interactive first-run setup wizard- Prompts for journal dir, storage dir, git remote/branch, default command, LLM URL and model
- Creates config file and all required directories; warns if config already exists
- Accepts
y/Y/yes/YESto confirm overwrite;--forceskips the prompt
mps config init— idempotent, non-interactive: writes every config key (including the fullchat:block) explicitly to the YAML file; safe to run any timemps config check— validates config health:- Checks
mps_dir,storage_dir,log_fileexist - Validates
type_aliasesresolve to known element types;command_aliasesresolve to known commands - TCP-checks
chat.urlreachability (skip with--no-network); HTTPS defaults to port 443 - Exits non-zero on any failure; ✓/✗ per item
- Checks
mps config show— now prints the fullchat:section (url, model, context_days, stream, api_key masked, sessions_dir)mps meta sync up/down— now includes non-sensitive chat fields (url,model,context_days,stream);api_keyandsessions_direxplicitly excludedChatConfiginconfig.rs(local-only:url,model,context_days,stream,api_key,sessions_dir)ChatMetaConfiginmeta.rs(synced:url,model,context_days,streamonly)- Total tests: 943 (47 new integration tests for init/config init/config check)
v1.7.7 (2026-05-27)
- CI fix: resolved all Clippy warnings-as-errors (
-D warnings) andrustfmtformatting failures - Total tests: 896
v1.7.6 (2026-05-25)
- Fix: Editing a task and setting status to
donewithout changing the body text incorrectly returned404 "ref not found" - Fix (defense-in-depth):
saveEdit()in the browser UI now only includesbodyin the PATCH payload if the value actually changed from what was fetched
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 - Fix: light-mode hover on element items was invisible
- Fix: font-family
<select>replaced withSans / Serif / Monobutton group - UX: Search tab auto-focuses;
Ctrl+Entersubmits form; Edit modal validates non-empty body; Today filter shows element count; raw 422 errors replaced with human-readable messages
v1.7.4 (2026-05-25)
- Font family selector:
Sans,Serif,Mono; persisted inlocalStorage
v1.7.3 (2026-05-25)
- Comprehensive UI revision: CSS variable–based font scaling, custom scrollbar, smooth focus rings,
fadeDownanimation, blur backdrop on modal - Font size controls (
A−/A/A+) persisted inlocalStorage - Colored left-border accent stripe per element type; redesigned stat pills; task completion progress bar
- Keyboard hints footer
v1.7.2 (2026-05-25)
- Light/dark theme toggle (🌙/☀️) persisted in
localStorage
v1.7.1 (2026-05-25)
- Browser UI served at
GET /— no external dependencies, fully self-contained dark-theme SPA GET /elements/:ref— fetch a single element by epoch or human refPATCH /elements/:refnow acceptsbodyfield for in-place body text editing
v1.7.0 (2026-05-25)
mps serve— Axum HTTP server with 9 JSON endpoints, Bearer token auth, CORS- Browser SPA with Today/Search/Archive/Tags/Stats tabs, dark theme, keyboard shortcuts
ServeConfigin~/.mps_config.yaml; 50 new tests
v1.6.2 (2026-05-25)
mps meta sync up/mps meta sync down— push/pull machine-agnostic settings via.mps.meta
v1.6.1 (2026-05-24)
- Fix reminder deduplication cooldown,
merge_meta()field-by-field notify merge,mps editbody dedenting, meta JSON validation, notify briefing line cap - 559 tests total
v1.6.0 (2026-05-24)
mps edit REFPATH— open element body in$EDITOR, atomic write-backmps delete REFPATH [--yes]— remove element with confirmation
v1.5.0 (2026-05-06)
mps notify,mps daemon— desktop notifications via systemd user timermps meta— cross-device config layer (.mps.meta/.mps.local)
License
MIT