# mps

[![Crates.io](https://img.shields.io/crates/v/mps-rs)](https://crates.io/crates/mps-rs)
[![CI](https://github.com/mash-97/mps-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/mash-97/mps-rs/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

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)](https://github.com/mash-97/mps). Fully backward-compatible with all Ruby-generated files and config.

---

## Install

```bash
cargo install mps-rs
```

The installed binary is named `mps`.

### From source

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

---

## Quick start

```bash
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.

```bash
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 |

```bash
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. `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 |

```bash
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.

```bash
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) |

```bash
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) |

```bash
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`.

```bash
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 |

```bash
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 |

```bash
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 |

```bash
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`

```bash
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.

```bash
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.

```bash
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](#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) |

```bash
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 |

```bash
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.

```bash
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 |

```bash
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.

```bash
# 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](#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).

```yaml
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 note
- `mps s "auth"` → searches for "auth"

Override the config path:

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

```json
{
  "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](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
