Metadata-Version: 2.4
Name: devlog-tool
Version: 2.0.0
Summary: A terminal-based developer journal tool
Author: Mico Arenga
License-Expression: MIT
Project-URL: Homepage, https://github.com/SpIob/devlog-cli
Project-URL: Repository, https://github.com/SpIob/devlog-cli
Project-URL: Issues, https://github.com/SpIob/devlog-cli/issues
Project-URL: Changelog, https://github.com/SpIob/devlog-cli/blob/main/CHANGELOG.md
Keywords: journal,cli,developer-tools,diary,devlog,notes
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.0
Requires-Dist: rich>=13.0
Requires-Dist: tomli>=1.1; python_version < "3.11"
Requires-Dist: tzdata
Provides-Extra: ai
Dynamic: license-file

# devlog-cli

[![CI](https://github.com/SpIob/devlog-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/SpIob/devlog-cli/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/devlog-tool)](https://pypi.org/project/devlog-tool/)
[![Python](https://img.shields.io/pypi/pyversions/devlog-tool)](https://pypi.org/project/devlog-tool/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/SpIob/devlog-cli/blob/main/LICENSE)

A terminal-based developer journal — log, search, filter, and export daily progress entries without leaving the command line. Local-AI powered: weekly digests, semantic search, and an MCP server for AI agents, all fully offline.

---

## Demo

```
$ devlog add "Rewrote auth middleware to use JWT refresh tokens" -t backend -t security
╭─ ✔ Entry added  ·  a1b2c3d4 ─────────────────────────────────────────────────────────────────────╮
│ Date  : 2025-05-11 10:22 UTC                                                                     │
│ Tags  : backend, security                                                                        │
│ Note  : Rewrote auth middleware to use JWT refresh tokens                                        │
│                                                                                                  │
│ Use `devlog show a1b2c3d4` to view it again, or `devlog edit a1b2c3d4` to amend it.             │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯

$ devlog list
                          Journal · 3 entries
╭──────────┬───────────────────────┬──────────────────────┬────────────────────────────────────────╮
│ ID       │ Date                  │ Tags                 │ Message                                │
├──────────┼───────────────────────┼──────────────────────┼────────────────────────────────────────┤
│ c3d4e5f6 │ 2025-05-11 14:30 UTC  │ bugfix, devops       │ Fixed flaky test in CI pipeline        │
│ b2c3d4e5 │ 2025-05-11 11:05 UTC  │ architecture, docs   │ Drafted ADR for new caching layer      │
│ a1b2c3d4 │ 2025-05-11 10:22 UTC  │ backend, security    │ Rewrote auth middleware to use JWT     │
│          │                       │                      │ refresh tokens                         │
├──────────┼───────────────────────┼──────────────────────┼────────────────────────────────────────┤
│          │                       │                      │ Showing 3 of 3 entries.                │
╰──────────┴───────────────────────┴──────────────────────┴────────────────────────────────────────╯

$ devlog search "auth"
                           Journal · 1 match
                          Query: "auth"
╭──────────┬───────────────────────┬──────────────────────┬────────────────────────────────────────╮
│ ID       │ Date                  │ Tags                 │ Message                                │
├──────────┼───────────────────────┼──────────────────────┼────────────────────────────────────────┤
│ a1b2c3d4 │ 2025-05-11 10:22 UTC  │ backend, security    │ Rewrote auth middleware to use JWT     │
│          │                       │                      │ refresh token…                         │
├──────────┼───────────────────────┼──────────────────────┼────────────────────────────────────────┤
│          │                       │                      │ Showing 1 of 1 entry.                  │
╰──────────┴───────────────────────┴──────────────────────┴────────────────────────────────────────╯
```

---

## Installation

**With pipx (recommended — keeps devlog isolated):**

```bash
pipx install devlog-tool
```

**With pip:**

```bash
pip install devlog-tool
```

**With the `ai` extra** (documents the AI tier — digest/ask/mcp; the features are built in and stdlib-only, you just need a local [Ollama](https://ollama.com) server for `--llm` and `ask`):

```bash
pipx install "devlog-tool[ai]"
ollama pull llama3.2 nomic-embed-text
```

**From source:**

```bash
pipx install git+https://github.com/SpIob/devlog-cli.git
```

After installation, the `devlog` command is available globally in your terminal.

Requires Python 3.10+.

---

## Quick Start

```bash
# Log what you did this morning
devlog add "Set up staging environment on Railway" -t devops

# Add an entry with multiple tags
devlog add "Fixed off-by-one error in pagination logic" -t bugfix -t backend

# Review today's work
devlog list

# Find everything related to your auth work
devlog search "auth"

# Narrow list output to a specific tag
devlog list -t bugfix

# Show the full text of a single entry by ID
devlog show a1b2c3d4

# Edit a typo in an entry's message
devlog edit a1b2c3d4 -m "Corrected message text"

# Backdate a forgotten entry
devlog add "Wrote the README yesterday" --at "2h"

# Delete an entry (prompts for confirmation; use -y to skip)
devlog delete a1b2c3d4 -y

# See which tags you use most
devlog tags

# Show all entries with a particular tag
devlog tag backend

# Strip a tag from every entry that has it
devlog tag backend --delete

# Merge two tags (add NEW to entries that have OLD, then remove OLD)
devlog merge-tag backend devops

# See what you logged today / yesterday
devlog today
devlog yesterday

# See the last 7 days
devlog week

# See the 10 most recent entries
devlog tail 10

# Get a quick journal summary with a 30-day sparkline
devlog stats

# See a year-grid heatmap of activity
devlog calendar

# Rename a tag across every entry
devlog rename-tag backend devops

# Customize the colors (optional)
devlog theme show > ~/.devlog/theme.toml
$EDITOR ~/.devlog/theme.toml
devlog theme set ~/.devlog/theme.toml

# Import entries from an exported Markdown file
devlog import ~/notes/old-log.md

# Launch the interactive REPL
devlog --interactive

# Export everything to a Markdown file for your weekly review
devlog export -o ~/notes/week-20.md
```

---

## Command Reference

### `devlog add`

Add a new journal entry.

```
devlog add MESSAGE [OPTIONS]
```

| Argument / Option | Short | Type | Required | Default | Description |
|-------------------|-------|------|----------|---------|-------------|
| `MESSAGE` | — | string | Yes | — | The body of the journal entry. |
| `--tag` | `-t` | string | No | — | Attach a tag to the entry. Repeatable: `-t backend -t bugfix`. Tags are normalized to lowercase. Allowed characters: `[a-z0-9-]`. |
| `--at` | — | string | No | — | Backfill the entry with a custom `created_at`. Accepts absolute timestamps (`YYYY-MM-DD`, `YYYY-MM-DDTHH:MM`, `…Z`) and relative ones (`2h`, `30m` ago). With `DEVLOG_TZ` set, naive inputs are interpreted in that zone. |
| `--quiet` | `-q` | flag | No | False | Suppress confirmation output. Exits silently with code 0 on success. |

**Notes:**

- Tag validation: see [Tag rules](#tag-rules).
- An empty message string exits with code 1.

**Examples:**

```bash
devlog add "Finished OAuth2 integration"
devlog add "Refactored database connection pool" -t backend -t refactor
devlog add "Deployed hotfix to production" -t devops -q
devlog add "Wrote the README yesterday" --at "2h"
devlog add "Logged a meeting from 9am" --at "2025-08-28T09:00:00Z"
```

---

### `devlog list`

Display journal entries in a formatted table, newest first.

```
devlog list [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--tag` | `-t` | string | None | Filter by tag. Repeatable. Multiple flags use AND logic — entry must carry all specified tags. |
| `--limit` | `-n` | integer | 20 | Maximum number of entries to display. Must be a positive integer. |
| `--all` | — | flag | False | Override `--limit` and display every entry. |
| `--since` | — | string | None | Only show entries on or after this date. See [Date filters](#date-filters). |
| `--until` | — | string | None | Only show entries on or before this date. See [Date filters](#date-filters). |
| `--quiet` | `-q` | flag | False | Output one raw JSON object per line to STDOUT instead of the Rich table. Useful for scripting. |

**Notes:**

- Results are sorted newest first.
- Message text is truncated to 60 characters with a `…` suffix in table view.
- If no entries exist, prints `No entries found.` and exits with code 0.
- If filters are applied and nothing matches, prints `No entries match your filters.` and exits with code 0.

**Examples:**

```bash
devlog list
devlog list -t bugfix
devlog list -t backend -t auth --limit 5
devlog list --all
devlog list --since 2025-01-01 --until 2025-12-31
devlog list --since 7d
devlog list --quiet | jq '.tags'
```

---

### `devlog search`

Full-text search across all entry messages.

```
devlog search QUERY [OPTIONS]
```

| Argument / Option | Short | Type | Required | Default | Description |
|-------------------|-------|------|----------|---------|-------------|
| `QUERY` | — | string | Yes | — | Search term. Case-insensitive substring match against the message field. Multiple space-separated terms require ALL of them (AND). Wrap a phrase in quotes to match it exactly. With `--regex`, QUERY is a regular expression. |
| `--tag` | `-t` | string | No | None | Narrow results to entries that also carry these tags. Repeatable; AND logic. |
| `--limit` | `-n` | integer | No | 20 | Maximum number of results to display. |
| `--since` | — | string | No | None | Only show entries on or after this date. See [Date filters](#date-filters). |
| `--until` | — | string | No | None | Only show entries on or before this date. See [Date filters](#date-filters). |
| `--regex` | — | flag | No | False | Interpret `QUERY` as a case-insensitive regular expression. Invalid patterns exit with code 2. |
| `--quiet` | `-q` | flag | No | False | Output raw JSON lines to STDOUT instead of the Rich table. |

**Notes:**

- Matching substrings are highlighted in bold yellow in the Message column (single-term queries only; highlighting is skipped for multi-term and regex queries).
- An empty or whitespace-only `QUERY` is rejected with `QUERY cannot be empty.` and exits with code 1.
- If no results are found, prints `No entries matched "<query>".` and exits with code 0.
- Tag filters and the search term both apply simultaneously (AND logic).

**Examples:**

```bash
devlog search "null pointer"
devlog search auth login              # AND: both terms required
devlog search "login bug" auth        # quoted phrase + term
devlog search "^Fix bug #[0-9]+$" --regex
devlog search "cache" -t backend
devlog search "deploy" --limit 10
devlog search "refactor" --since 7d
devlog search "refactor" --quiet | jq '.'
```

---

### `devlog on`

Show entries for a specific date — the missing sibling of `today` / `yesterday` / `week`.

```
devlog on DATE [OPTIONS]
```

| Argument / Option | Short | Type | Required | Default | Description |
|-------------------|-------|------|----------|---------|-------------|
| `DATE` | — | string | Yes | — | `today`, `yesterday`, `Nd` (N days ago, e.g. `3d`), or an absolute `YYYY-MM-DD`. |
| `--limit` | `-n` | integer | No | 50 | Maximum entries to show. |
| `--quiet` | `-q` | flag | No | False | Output raw JSON lines. |

**Notes:**

- The day bucket is the local date when `DEVLOG_TZ` is set, otherwise UTC — same rule as `today`/`yesterday`/`week`.
- An unparseable `DATE` exits with code 2.

**Examples:**

```bash
devlog on today
devlog on yesterday
devlog on 3d                # three days ago
devlog on 2025-05-10
```

---

### `devlog show`

Display a single entry in full detail (no message truncation).

```
devlog show ID
```

| Argument / Option | Short | Type | Required | Default | Description |
|-------------------|-------|------|----------|---------|-------------|
| `ID` | — | string | Yes | — | The 8-char short id, the full UUID, or a unique prefix of either. |
| `--quiet` | `-q` | flag | No | False | Output a single raw JSON line instead of the panel. |

**Notes:**

- If the prefix matches more than one entry, the command exits with code 1 and lists the candidate short ids.
- The full message is shown (no 60-char truncation).
- `Updated` displays `—` for entries that have never been edited, otherwise the timestamp of the most recent edit.

**Examples:**

```bash
devlog show a1b2c3d4
devlog show a1b2
devlog show a1b2c3d4-e5f6-7890-abcd-ef1234567890 --quiet
```

---

### `devlog edit`

Edit an entry's message and/or tags in place. Preserves the original `created_at` (unless `--at` is supplied) and sets `updated_at` on success.

```
devlog edit ID [OPTIONS]
```

| Argument / Option | Short | Type | Required | Default | Description |
|-------------------|-------|------|----------|---------|-------------|
| `ID` | — | string | Yes | — | The 8-char short id, the full UUID, or a unique prefix. |
| `--message` | `-m` | string | No | — | Replace the message text. |
| `--tag` | `-t` | string, repeatable | No | — | Replace the tag set with this list. Existing tags are discarded. |
| `--add-tag` | — | string, repeatable | No | — | Append tags to the existing set. Duplicates are silently dropped. |
| `--remove-tag` | — | string, repeatable | No | — | Remove tags from the existing set. |
| `--at` | — | string | No | — | Replace the entry's `created_at`. Same format as `add --at`. Prompts for confirmation unless `--yes` is also passed. |
| `--yes` | `-y` | flag | No | False | Skip the `--at` confirmation prompt. |
| `--quiet` | `-q` | flag | No | False | Suppress confirmation output. |

**Notes:**

- If no flag is passed, the entry's current message is opened in `$VISUAL` / `$EDITOR` (falling back to `nano` or `vi` if neither is set). Save and exit to apply the edit; abort to discard. This requires a TTY; in a non-interactive shell (CI, scripts, output captured to a file), `edit` with no flags errors out instead of dumping terminal escapes. Set `DEVLOG_ALLOW_EDITOR_IN_PIPE=1` to opt in to the old behaviour.
- An empty `--message` is rejected with the same `MESSAGE cannot be empty` error as `devlog add`. The editor path is allowed to produce an empty body, since the user may want to save a blank note on purpose.
- No-op edits (the result equals the existing entry) print `No changes.` and exit 0 without touching the file.
- Tag flags combine: `--tag` runs first, then `--add-tag` adds, then `--remove-tag` removes.
- Validation rules for tag characters and length: see [Tag rules](#tag-rules).

**Examples:**

```bash
devlog edit a1b2c3d4 -m "Corrected typo in the original entry"
devlog edit a1b2c3d4 --add-tag urgent
devlog edit a1b2c3d4 --remove-tag docs --add-tag refactor
devlog edit a1b2c3d4     # opens $EDITOR with the current message
devlog edit a1b2c3d4 --at "2025-08-28T09:00:00Z" --yes   # re-time without prompt
```

---

### `devlog delete`

Delete an entry by id.

```
devlog delete ID [OPTIONS]
```

| Argument / Option | Short | Type | Required | Default | Description |
|-------------------|-------|------|----------|---------|-------------|
| `ID` | — | string | Yes | — | The 8-char short id, the full UUID, or a unique prefix. |
| `--yes` | `-y` | flag | No | False | Skip the confirmation prompt. |
| `--quiet` | `-q` | flag | No | False | Suppress the success panel. |
| `--ignore-missing` | — | flag | No | False | Exit 0 (not 1) when the given ID does not exist. Useful in scripts where `delete` should be idempotent. |

**Notes:**

- By default, the command asks for confirmation (`[y/N]`). Decline to abort without changes; confirm to delete and render a red-bordered panel.
- Deletion is atomic — the JSON file is rewritten via the same temp-file + `os.replace()` pattern as every other write.

**Examples:**

```bash
devlog delete a1b2c3d4              # confirm interactively
devlog delete a1b2c3d4 -y           # skip prompt
devlog delete a1b2 -y               # delete by unique prefix
devlog delete a1b2c3d4 -y --ignore-missing   # idempotent: no error if absent
```

---

### `devlog tags`

List all distinct tags with their usage count and the most recent date they were used.

```
devlog tags [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--sort` | — | choice | `count` | One of `count` (most-used first), `name` (alphabetical), or `recent` (most recently used first). |
| `--limit` | `-n` | integer | 50 | Maximum number of tags to show. |
| `--all` | — | flag | False | Override `--limit` and show every tag. |
| `--quiet` | `-q` | flag | False | Output `{tag, count, last_used}` JSON lines instead of the table. |

**Notes:**

- `Last used` is the more recent of the entry's `created_at` and `updated_at`, so editing a tag keeps its timestamp fresh.
- Empty state: prints `No tags found.` and exits 0.

**Examples:**

```bash
devlog tags
devlog tags --sort name
devlog tags --limit 10
devlog tags --quiet | jq '.tag'
```

---

### `devlog theme`

View or change the active color theme. Themes live in `~/.devlog/theme.toml` (or `$DEVLOG_DATA_DIR/theme.toml` when set) and override individual color roles. See the [Themes](#themes) section for the full list of roles and a starter file.

```
devlog theme SUBCOMMAND
```

| Subcommand | Description |
|------------|-------------|
| `devlog theme list` | Print every role and its current style as a two-column table (with section grouping and preview swatches by default). |
| `devlog theme show [ROLE]` | Print the value of a single role, or dump a starter `theme.toml` (all roles commented out) to STDOUT. |
| `devlog theme set PATH` | Validate and install a theme file as the active theme. Unknown roles are dropped with a warning. Add `--check` to validate without installing. |
| `devlog theme use NAME` | Install a bundled theme by name (e.g. `devlog theme use dracula`). Use `devlog theme use default` to reset to the built-in defaults. |
| `devlog theme builtins` | List all bundled theme names with their short description. |
| `devlog theme reset` | Remove the active theme file and fall back to the defaults. Prompts for confirmation. |
| `devlog theme edit` | Open the active theme file in `$VISUAL`/`$EDITOR`. Creates a starter template if no file exists yet. |
| `devlog theme create` | Interactively walk through every role, preview the palette, and install it as the active theme (or write to `--output PATH`). |
| `devlog theme diff` | Show the active theme's overrides relative to the built-in defaults. With `--default`, inverts to show roles that still match the defaults. |
| `devlog theme export` | Write the active palette as a complete, uncommented `theme.toml` to `--output PATH` or `--stdout`. |
| `devlog theme path` | Print the absolute path to the active theme file. |

**Notes:**

- `theme list` accepts `--flat` (disable section grouping) and `--no-preview` (hide the preview swatch column).
- `theme set` accepts `--check` to validate a theme file without installing it (exits 1 on any error).
- `theme create` accepts `--from BUNDLED_NAME` to seed values from a built-in theme, `--name`/`--description` for `[meta]`, `--output PATH` to write to a file instead of installing, and `--install`/`--no-install` to control installation explicitly.
- `theme export` requires exactly one of `--stdout` or `--output PATH`.

**Examples:**

```bash
# See every color role (grouped, with preview swatches)
devlog theme list
devlog theme list --flat --no-preview   # compact, single column

# Read a single role's value
devlog theme show date
# → cyan

# Generate a starter theme file you can edit
devlog theme show > ~/my-theme.toml
$EDITOR ~/my-theme.toml   # uncomment and tweak the roles you want to change
devlog theme set ~/my-theme.toml
devlog theme set ~/my-theme.toml --check   # validate only, don't install

# Try a bundled theme
devlog theme builtins
devlog theme use dracula
devlog theme use default                   # reset to built-in defaults

# Find the active theme path (useful for `ln -s` workflows)
devlog theme path
# → /Users/you/.devlog/theme.toml

# Edit the active theme in your $EDITOR
devlog theme edit

# Build a new theme interactively
devlog theme create --from nord --name "My Nord Variant"
devlog theme create --output ~/my-theme.toml --no-install

# Audit your overrides
devlog theme diff          # show only roles that differ from defaults
devlog theme diff --default    # show only roles that still match defaults

# Export the active palette
devlog theme export --output ~/devlog-palette.toml
devlog theme export --stdout > ~/devlog-palette.toml
```

---

### `devlog today`

Show entries created today, newest first.

```
devlog today [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--limit` | `-n` | integer | 50 | Maximum entries to show. |
| `--quiet` | `-q` | flag | False | Output raw JSON lines instead of a table. |

**Notes:**

- "Today" is computed in the local zone when `DEVLOG_TZ` is set, otherwise UTC. Set `DEVLOG_TZ=America/New_York` to make `today`, `yesterday`, and `week` reflect your wall clock.
- Empty state: prints `No entries yet today.` and exits 0.

**Examples:**

```bash
devlog today
devlog today --limit 5
devlog today --quiet
```

---

### `devlog yesterday`

Show entries created yesterday, newest first. Same local-tz semantics as `today`.

```
devlog yesterday [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--limit` | `-n` | integer | 50 | Maximum entries to show. |
| `--quiet` | `-q` | flag | False | Output raw JSON lines instead of a table. |

**Examples:**

```bash
devlog yesterday
devlog yesterday --quiet
```

---

### `devlog week`

Show entries from the last 7 days, newest first. The window ends on today (or the `--day` anchor) and spans the 6 days before it.

```
devlog week [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--limit` | `-n` | integer | 100 | Maximum entries to show. |
| `--day` | — | string (YYYY-MM-DD) | today | Anchor day; the week ends on this day and includes the 6 days before it. |
| `--quiet` | `-q` | flag | False | Output raw JSON lines instead of a table. |

**Examples:**

```bash
devlog week
devlog week --day 2025-05-10
devlog week --limit 20 --quiet
```

---

### `devlog tail`

Show the N most recent entries (default 5), newest first.

```
devlog tail [N] [OPTIONS]
```

| Argument / Option | Short | Type | Default | Description |
|-------------------|-------|------|---------|-------------|
| `N` | — | integer | 5 | Number of entries to show. |
| `--tag` | `-t` | string, repeatable | None | Filter by tag (AND logic). |
| `--quiet` | `-q` | flag | False | Output raw JSON lines. |

**Examples:**

```bash
devlog tail        # last 5
devlog tail 20
devlog tail 10 -t bugfix
```

---

### `devlog stats`

Show a one-glance summary of the journal: totals, date range, current and longest writing streaks, top tags and tag pairs, days since the last entry, and a 30-day sparkline of entries per day.

```
devlog stats [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--quiet` | `-q` | flag | False | Output a single JSON object with the summary. |
| `--since` | — | string | None | Only include entries on or after this date. See [Date filters](#date-filters). |
| `--until` | — | string | None | Only include entries on or before this date. See [Date filters](#date-filters). |

**Notes:**

- The sparkline is an ASCII bar (`▁`–`█`) representing entries per day for the last 30 days. The window and date range are computed in the local zone when `DEVLOG_TZ` is set, otherwise UTC.
- The `First` / `Last` / `Span` lines render in the local zone when `DEVLOG_TZ` is set (the `… UTC` suffix becomes the zone's key, e.g. `… EST`).
- The `Streak` line shows your **current** consecutive-day streak (counted back from today; writing yesterday but not today keeps it alive) plus the longest streak in journal history. `Last entry: N days ago` is the nudge line.
- `Top tag pairs` lists the tag co-occurrences — the two tags most often written on the same entry.
- `--quiet` adds `streaks` (`{"current": N, "longest": N}`), `top_tag_pairs` (`[{"tags": [a, b], "count": N}]`), and `days_since_last` to the JSON object.
- Empty state: prints `No entries to summarize.` and exits 0.

**Example output:**

```
╭─ Journal Stats ─────────────────────────────────────────╮
│                                                       │
│  Total     : 142                                      │
│  First     : 2025-04-12 09:14 UTC                     │
│  Last      : 2026-08-29 11:02 UTC                     │
│  Span      : 139 days                                 │
│  Avg/day   : 1.02                                     │
│                                                       │
│  Top 5 tags                                           │
│    backend : 41                                       │
│    bugfix  : 28                                       │
│    docs    : 17                                       │
│    devops  : 12                                       │
│    refactor: 9                                        │
│                                                       │
│  Last 30 days (entries per day)                          │
│  ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█▁▁█▁█                     │
╰───────────────────────────────────────────────────────╯
```

**Examples:**

```bash
devlog stats
devlog stats --quiet | jq '.total, .top_tags'
```

---

### `devlog tag`

Show entries that carry a tag, or strip a tag from every entry that has it. (No entries are deleted by `--delete`.)

```
devlog tag NAME [OPTIONS]
```

| Argument / Option | Short | Type | Required | Default | Description |
|-------------------|-------|------|----------|---------|-------------|
| `NAME` | — | string | Yes | — | The tag to inspect. Normalised to lowercase. |
| `--delete` | — | flag | False | Strip the tag from every entry that has it (prompts for confirmation; use `-y` to skip). |
| `--yes` | `-y` | flag | False | With `--delete`: skip the confirmation prompt. |
| `--dry-run` | — | flag | False | With `--delete`: show what would change without writing. |
| `--limit` | `-n` | integer | 20 | Show mode: max entries to list. |
| `--all` | — | flag | False | Show mode: override `--limit` and list every entry. |
| `--quiet` | `-q` | flag | False | Output raw JSON lines (or suppress the success line under `--delete`). |

**Notes:**

- `NAME` is validated with the same rules as `devlog add` — see [Tag rules](#tag-rules).
- `devlog tag NAME --delete` asks `Remove tag "NAME" from N entries?` by default — pass `-y`/`--yes` to skip, or combine with `--quiet` to skip the prompt in scripts.
- `devlog tag NAME --delete --dry-run` previews the change without writing.
- Use `devlog merge-tag A B` if you want to *combine* two tags instead of just inspecting or removing one.

**Examples:**

```bash
devlog tag backend
devlog tag backend --limit 5
devlog tag backend --delete -y
devlog tag backend --delete --dry-run
```

---

### `devlog calendar`

Show a year-grid heatmap of journal activity. Each cell is one day; the colour tier reflects the entry count for that day.

```
devlog calendar [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--year` | — | integer | current year | Year to render. |
| `--quiet` | `-q` | flag | False | Output a `{YYYY-MM-DD: count}` JSON map for the year. |

**Notes:**

- The grid uses the `heatmap_empty` / `heatmap_l1` … `heatmap_l4` theme roles. See [Themes](#themes) to restyle.
- The bucketing date is the local date when `DEVLOG_TZ` is set, otherwise UTC. With `DEVLOG_TZ=America/New_York`, an entry at 02:00 UTC on Jan 1 lands in Dec 31's cell.
- Empty state: prints `No entries in <year>.` and exits 0.

**Examples:**

```bash
devlog calendar
devlog calendar --year 2024
devlog calendar --year 2024 --quiet | jq
```

---

### `devlog rename-tag`

Rename a tag across every entry in the journal (bulk replace).

```
devlog rename-tag OLD NEW [OPTIONS]
```

| Argument / Option | Short | Type | Required | Default | Description |
|-------------------|-------|------|----------|---------|-------------|
| `OLD` | — | string | Yes | — | The existing tag to replace. |
| `NEW` | — | string | Yes | — | The new tag name. Validated as a tag (see [Tag rules](#tag-rules)). |
| `--dry-run` | — | flag | No | False | Show the count of affected entries without writing. |
| `--quiet` | `-q` | flag | No | False | Suppress the success line. |

**Notes:**

- Every entry that contains `OLD` is rewritten in place. The entry's `created_at` is preserved and `updated_at` is set to the current UTC time.
- If an entry already carries `NEW`, the result is deduplicated so no entry ends up with the same tag twice.
- If no entries carry `OLD`, prints `No entries with tag "<old>".` and exits 0.
- `NEW` is validated against the tag rules (see [Tag rules](#tag-rules)) and rejected with a clear error if it would be invalid. (Older versions used to silently no-op when the lowercased NEW happened to equal OLD — that path is fixed.)

**Examples:**

```bash
devlog rename-tag backend devops
devlog rename-tag bugfix fix --dry-run
```

---

### `devlog merge-tag`

Merge `OLD` into `NEW` across every entry: every entry that has `OLD` gets `NEW` added (deduplicated) and `OLD` removed. Entries that already carry `NEW` are not double-tagged.

```
devlog merge-tag OLD NEW [OPTIONS]
```

| Argument / Option | Short | Type | Required | Default | Description |
|-------------------|-------|------|----------|---------|-------------|
| `OLD` | — | string | Yes | — | The tag to retire. |
| `NEW` | — | string | Yes | — | The tag to consolidate into. Validated as a tag (see [Tag rules](#tag-rules)). |
| `--dry-run` | — | flag | False | Show the count of affected entries without writing. |
| `--quiet` | `-q` | flag | False | Suppress the success line. |

**Notes:**

- If `OLD.lower() == NEW.lower()` after normalisation, the command prints `OLD and NEW are the same ("…"). No changes made.` and exits 0.
- If no entries carry `OLD`, prints `No entries with tag "OLD".` and exits 0.
- For a pure rename use `devlog rename-tag`. For stripping a tag entirely, use `devlog tag <name> --delete`.

**Examples:**

```bash
devlog merge-tag backend devops
devlog merge-tag bugfix fix --dry-run
```

---

### `devlog import`

Import entries from a JSON file (the native `entries.json` shape), from a Markdown file produced by `devlog export`, or from a **jrnl** or **Day One** JSON export.

```
devlog import PATH [OPTIONS]
```

| Argument / Option | Short | Type | Required | Default | Description |
|-------------------|-------|------|----------|---------|-------------|
| `PATH` | — | path | Yes | — | Input file. Must exist and be readable. |
| `--format` | `-f` | choice | `auto` | One of `auto`, `json`, `markdown`, `jrnl`, `dayone`. `auto` picks by file extension, then by content sniffing. |
| `--dry-run` | — | flag | False | Show what would be imported without writing. |
| `--quiet` | `-q` | flag | False | Suppress the summary line. |

**Notes:**

- Re-importing the same file is safe and idempotent: entries are recognised by their existing `id` (when present in the source) or by their `(created_at, message)` fingerprint, so the second import is a no-op.
- Stable `id` values in the source JSON are preserved across re-imports, so a `devlog export` → `devlog import` round-trip keeps short ids stable for cross-referencing. If the source row omits `id`, devlog mints a fresh one.
- Unreadable rows in a JSON import (non-dict items or entries missing required fields) are reported in the summary line as `Ignored N unreadable rows.`
- Auto-detect sniffs the first non-blank character when the file extension is missing, so pipes and extensionless files work: leading `{` → JSON, leading `#` → Markdown.
- **jrnl** format: an object with an `entries` array whose items carry `text`/`date`/`starred`. `text` becomes the message, `date` (naive timestamps are treated as UTC) becomes `created_at`, `tags` carry over, and a starred entry gains a `starred` tag. Re-imports dedupe by fingerprint since jrnl rows have no stable ids.
- **Day One** format: an object with an `entries` array whose items carry `creationDate`/`text`. The Day One `uuid` is preserved as the entry id; tags are lowercased with spaces converted to hyphens.
- If the file format cannot be detected and `--format` was not given, exits with code 2.
- Malformed JSON or markdown that contains no `## …` headings exits with code 2.

**Examples:**

```bash
devlog import ~/notes/old-log.md
devlog import /tmp/entries.json --format json
devlog import ~/.jrnl/journal.json --format jrnl     # or just: devlog import ~/.jrnl/journal.json
devlog import ~/Downloads/Day\\ One.json              # auto-detects Day One
devlog import /tmp/entries.json --dry-run
```

---

### `devlog git`

Git integration for keeping your journal inside a repository (sync + history for free).

```
devlog git {status|sync|log} [OPTIONS]
```

| Command | Option | Type | Default | Description |
|---------|--------|------|---------|-------------|
| `git status` | `--json` | flag | False | Emit the full status (repo root, branch, remote, clean flag, changed files) as one JSON object. |
| `git sync` | `--message` / `-m` | string | `devlog: journal update` | Commit message used when changes are found. |
| `git sync` | `--quiet` / `-q` | flag | False | Suppress output. |
| `git log` | `--oneline` | flag | False | One commit per line. |
| `git log` | `--limit` / `-n` | integer | 20 | Maximum commits to show. |

**Notes:**

- Commands resolve the repository containing your data dir (`DEVLOG_DATA_DIR` or `~/.devlog`) — the data dir itself does not need to be the repo root; a journal subdirectory inside a repo works.
- `git status` reports `is_git_repo: false` (exit 0) when no repository is found.
- `git sync` runs `git add -A` + `git commit` only when `git status --porcelain` is non-empty; a clean repo is a no-op. Exits 1 when the data dir is not inside a repository.
- `git log` prints the repository's commit log (not filtered to journal files). A repository with no commits exits 1 with git's `does not have any commits yet` error.

**Example recipe — journal inside an existing repo:**

```bash
# Keep the journal in a private repo and commit after each entry.
export DEVLOG_DATA_DIR="$HOME/Dropbox/devlog-repo"
git -C "$HOME/Dropbox/devlog-repo" init   # once

devlog add "shipped the thing"
devlog git sync                            # commits journal changes
devlog git status                          # dirty state at a glance
```

**Example recipe — auto-commit after every write (shell hook):**

```bash
# In your shell rc: sync the journal whenever devlog writes to it.
devlog() {
  command devlog "$@" || return $?
  case "$1" in
    add|edit|delete|import|restore|merge-tag|rename-tag) command devlog git sync -q ;;
  esac
}
```

---

### Git-native entries: `devlog add --from-commit`

Turn a commit into a journal entry — the message, tags, and timestamp come from the commit itself.

```
devlog add [NOTE] --from-commit REF
```

| Argument / Option | Short | Type | Required | Default | Description |
|-------------------|-------|------|----------|---------|-------------|
| `NOTE` | — | string | Yes* | — | Optional note prefixed to the commit subject. Pass an empty string (`""`) to use the commit message alone. *Required by the parser; `""` is the "no note" idiom. |
| `--from-commit` | — | git ref | — | None | Any ref `git show` accepts: `HEAD`, `HEAD~1`, a sha, a branch name. |
| `--tag` / `-t` | — | string | No | None | Extra tags, merged in front of the ones extracted from the commit. |
| `--at` | — | timestamp | No | author date | Override the entry timestamp (same formats as a plain `add --at`). |

**Notes:**

- Message: the commit subject (first line) is used; a NOTE argument is prefixed as `NOTE\n\n<subject>`.
- Tags: a conventional-commit scope (`feat(auth): …`, `fix(api): …`) becomes the tag `auth`/`api`; a `[tag]` message prefix also becomes a tag; a `commit-<short-hash>` tag is always added for cross-referencing back to `git log`.
- Timestamp: the commit's author date, normalised to UTC.
- Must be run inside a git repository; exits 1 outside one or with an unresolvable ref.

**Examples:**

```bash
# Journal the commit you just shipped
devlog add "" --from-commit HEAD

# With a note and extra tags
devlog add "reviewed with the team" --from-commit HEAD~2 -t retro

# Journal the whole day's work from git history
for ref in $(git log --since=midnight --format=%h); do
  devlog add "" --from-commit "$ref" -q
done
```

---

### `devlog completions`

Print a shell completion script to STDOUT. Source or install it according to your shell's conventions.

```
devlog completions {bash|zsh|fish}
```

**Examples:**

```bash
# bash
devlog completions bash > ~/.bash_completion.d/devlog

# zsh (place in a directory on $fpath)
devlog completions zsh > ~/.zsh/completions/_devlog

# fish
devlog completions fish > ~/.config/fish/completions/devlog.fish
```

---

### `devlog --interactive`

Launch a minimal line-based REPL for browsing and adding entries without leaving the prompt.

```
devlog --interactive
devlog -i
DEVLOG_INTERACTIVE=1 devlog
```

**REPL commands** (run `help` at the prompt to see the up-to-date list — every command in the CLI is available, plus a few aliases):

| Command | What it does |
|---------|--------------|
| `add <message> [-t tag ...]` | Add a new entry. |
| `l \| list [-t tag] [-n N]` | List entries. |
| `s \| search <query>` | Search entry messages. |
| `show <id>` | Show a single entry. |
| `edit <id> [-m msg] [-t tag]` | Edit an entry. |
| `delete <id> [-y]` | Delete an entry. |
| `today` | Show today's entries. |
| `y \| yesterday` | Show yesterday's entries. |
| `w \| week` | Show the current week. |
| `tail [N]` | Show the N most recent entries. |
| `tags` | List tags with usage counts. |
| `stats` | Summarize the journal. |
| `tag <name>` | List entries with a tag (or `--delete -y` to remove the tag from every entry). |
| `merge-tag <old> <new>` | Rename one tag everywhere. |
| `calendar` | Year-grid heatmap of journal activity. |
| `theme` | View or change the active color theme (sub: `list`, `show`, `set`, `path`, `reset`, `edit`, `diff`, `export`, `use`, `builtins`, `create`). |
| `rename-tag <old> <new>` | Rename a tag. |
| `import <path>` | Import entries. |
| `completions <shell>` | Print a shell completion script. |
| `export [-o path]` | Export to Markdown or JSON. |
| `repair [-y] [--dry-run]` | Inspect and repair the on-disk store. |
| `backup [-o path]` | Write a timestamped backup. |
| `restore <path>` | Restore from a backup file. |
| `doctor` | Check store health. |
| `h \| help` | Show the REPL help. |
| `q \| quit \| exit` | Leave the REPL. |

**Notes:**

- Requires a TTY. Outside a TTY the command exits with code 1 and a clear error message. Set `DEVLOG_INTERACTIVE_FORCE=1` to bypass the TTY check (useful in tests, scripts, or remote shells).
- Any other subcommand argument is dispatched to the underlying CLI in-process.

**Examples:**

```bash
devlog --interactive
DEVLOG_INTERACTIVE=1 devlog
```

---

### `devlog export`

Export all entries (or a filtered subset) to a Markdown or JSON file.

```
devlog export [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--output` | `-o` | path | `<data-dir>/exports/devlog-YYYYMMDD-HHMMSS.<ext>` | Destination file path. Existing files are overwritten without prompting. When omitted, files land in `$DEVLOG_DATA_DIR/exports/` (or `~/.devlog/exports/`) rather than the current working directory, so `cd`-ing around doesn't pollute the cwd. |
| `--format` | `-f` | `auto` \| `markdown` \| `json` | `auto` | Output format. `auto` infers from the `--output` extension (`.json` → JSON, `.md`/`.markdown` → Markdown, anything else → Markdown). Explicit `--format` wins over the extension. |
| `--tag` | `-t` | string | None | Export only entries that carry all specified tags. Repeatable; AND logic. |
| `--since` | — | string | None | Only export entries on or after this date. See [Date filters](#date-filters). |
| `--until` | — | string | None | Only export entries on or before this date. See [Date filters](#date-filters). |
| `--quiet` | `-q` | flag | False | Suppress the progress bar and confirmation. Prints only the output path on success. |

**Output format per entry:**

```markdown
## 2025-05-11 10:22 UTC — a1b2c3d4

Rewrote auth middleware to use JWT refresh tokens.

**Tags:** backend, security

---
```

**Notes:**

- A Rich progress bar is shown on STDERR during the write (suppress with `--quiet`).
- If no entries exist (or no entries match the tag filters), prints `Warning: No entries to export.` and exits with code 0. **No file is created in that case**, so you can safely re-run after emptying the journal.
- If the output path is not writable, exits with code 2 and a clear error message.

**Examples:**

```bash
devlog export
devlog export -o ~/notes/week-20.md
devlog export -t backend -o backend-log.md
devlog export --since 2025-01-01 --until 2025-12-31 -o year.md
devlog export --quiet -o /tmp/devlog.md
```

---

### `devlog repair`

Inspect the on-disk journal and, optionally, rewrite it to drop malformed rows. Useful when `devlog doctor` reports validation issues, or when the file has been hand-edited and broken.

```
devlog repair [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--dry-run` | — | flag | False | Show the issues and what would be dropped, without writing. |
| `--yes` | `-y` | flag | False | Skip the "drop N entries — continue?" confirmation. |
| `--backup` / `--no-backup` | — | flag | `--backup` | Write a timestamped backup to `<data-dir>/backups/` before the rewrite. |
| `--quiet` | `-q` | flag | False | Suppress the summary panel. |

**Notes:**

- A backup is written *before* the file is rewritten, so you can always undo a repair with `devlog restore <backup-file>`.
- Validation rules match the storage contract: every entry must have a non-empty `id` and `message`, a parseable `created_at` (ISO 8601 UTC), and tags that match `^[a-z0-9-]+$` with at most 32 characters.
- Duplicate ids keep the first occurrence; subsequent duplicates are reported and dropped.
- Exit code is `0` when nothing was dropped, `1` when entries were dropped.
- A corrupted JSON file (invalid syntax) is not repairable; the command exits with code 2 and tells you to restore from a backup.

**Examples:**

```bash
# See what's wrong, without writing
devlog repair --dry-run

# Drop invalid rows, writing a backup first
devlog repair -y

# Run the repair non-interactively from a script
devlog repair -y --no-backup
```

---

### `devlog backup`

Write a timestamped copy of the journal. The default destination is `<data-dir>/backups/entries-YYYYMMDD-HHMMSS.json`. The file is a normal `entries.json` — it can be inspected, edited, and round-tripped through `devlog restore`.

```
devlog backup [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--output` | `-o` | path | `<data-dir>/backups/entries-TIMESTAMP.json` | Backup file path. Parent directories are created if they don't exist. |
| `--quiet` | `-q` | flag | False | Print only the backup path (no success panel). |

**Notes:**

- The backup is just a copy of `entries.json` (the JSON shape that `devlog` reads on every startup). It can be inspected, edited, and used as input to `devlog restore`.
- Backups are placed under `<data-dir>/backups/` by default; that directory is created lazily on first use.
- Empty journals can be backed up too — the resulting file is `{"entries": []}`.

**Examples:**

```bash
# Default: timestamped file under the data dir
devlog backup

# Custom path
devlog backup -o ~/archives/devlog-$(date +%F).json

# Scripting: capture the path, then move the file
devlog backup --quiet
```

---

### `devlog undo`

Roll the journal back to its state *before* the last write. Every mutating command (`add`, `edit`, `delete`, `tag`, `merge-tag`, `rename-tag`, `import`, `repair`, `restore`, …) snapshots the previous store into `<data-dir>/backups/entries-TIMESTAMP.pre.json` before writing — on both the JSON and SQLite backends — so a single `devlog undo` reverts any of them.

```
devlog undo [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--yes` | `-y` | flag | False | Skip the confirmation prompt. |
| `--keep` | — | int | 10 | Pre-write snapshots to retain after pruning (`0` removes all). |
| `--dry-run` | — | flag | False | Show what would be restored, without writing. |
| `--quiet` | `-q` | flag | False | Suppress the summary output. |

**Notes:**

- Repeated `undo` calls **step back** through recent writes one at a time (each undo consumes its snapshot instead of re-snapshotting the pre-undo state), so no toggle behavior.
- Pre-write snapshots are pruned to the 10 newest after every undo; manual `devlog backup` files are never touched or consumed.
- To *redo* after an undo, restore a manual backup: run `devlog backup` before experimenting, then `devlog restore <file>`.

**Examples:**

```bash
# Oops — that import duplicated everything
devlog undo

# Inspect first
devlog undo --dry-run

# Non-interactive (scripts)
devlog undo -y -q
```

---

### `devlog restore`

Replace the current journal with the contents of a backup file. Per-row issues in the backup are skipped with a warning, so a hand-edited or partially-corrupted backup can still be partially restored.

```
devlog restore PATH [OPTIONS]
```

| Argument / Option | Short | Type | Required | Default | Description |
|-------------------|-------|------|----------|---------|-------------|
| `PATH` | — | path | Yes | — | Backup file. Must exist and be readable. |
| `--yes` | `-y` | flag | No | False | Skip the "overwrite current journal?" confirmation. |
| `--dry-run` | — | flag | No | False | Validate the backup and report what would be restored, without writing. |
| `--quiet` | `-q` | flag | No | False | Suppress the success line. |

**Notes:**

- A non-empty journal prompts for confirmation by default; pass `-y` to skip the prompt in scripts.
- If the backup is structurally invalid (root is not a JSON object, or `entries` is not a list), the command refuses to restore and exits with code 2.
- Per-row issues (bad tags, unparseable timestamps, duplicate ids) are reported as warnings and skipped; the rest is restored.
- Restoring from an empty backup empties the journal — use `--dry-run` first if you're unsure.

**Examples:**

```bash
devlog restore ~/archives/devlog-2025-01-15.json
devlog restore backups/entries-20260829-080713.json -y
devlog restore some-backup.json --dry-run
```

---

### `devlog doctor`

Check the journal store for corruption and report a quick health snapshot: file path, size, entry count, days since the most recent entry, validation issues, and the three longest messages.

```
devlog doctor [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--quiet` | `-q` | flag | False | Output a single JSON object with the report. |

**Notes:**

- Exit code: `0` when the store is clean, `1` when validation issues are found (run `devlog repair` to fix), `2` when the file is corrupt or unwritable.
- "Days since last entry" is computed in UTC.
- The probe writes a one-byte file inside the data dir to confirm write access; it removes the file before exiting.

**Example output:**

```
╭─ 🩺 Doctor  ·  all clear ────────────────────────────────────────────────────╮
│ Path  : /Users/you/.devlog/entries.json                                       │
│ Exists: yes                                                                   │
│ Size  : 4.2 KB                                                                │
│ Writable: yes                                                                 │
│ Entries: 142                                                                  │
│ Last entry: today                                                             │
│                                                                               │
│ ✔ No validation issues.                                                       │
│                                                                               │
│ Longest messages:                                                             │
│   • a1b2c3d4… — 248 chars                                                     │
│   • c3d4e5f6… — 192 chars                                                     │
│   • 9f8e7d6c… — 165 chars                                                     │
╰───────────────────────────────────────────────────────────────────────────────╯
```

**Examples:**

```bash
devlog doctor
devlog doctor --quiet | jq '{ok, entry_count, issues: (.issues | length)}'
```

---

### `devlog digest --week`

Summarize recent journal activity. The default is a **fully offline, extractive** digest — entries grouped by local day with per-entry times, tags, and a tag-totals footer — no model, no network. With `--llm` the extractive text is handed to a local [Ollama](https://ollama.com) model for a written summary.

```
devlog digest --week [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--week` | — | flag | True | Summarize the last 7 days (the only period in this release). |
| `--day` | — | string | today | Anchor day (YYYY-MM-DD); the week ends on this day and spans the 6 before it. |
| `--llm` | — | flag | False | Also produce an LLM-written summary via local Ollama. |
| `--model` | — | string | `llama3.2` | Ollama model for `--llm`. |
| `--quiet` | `-q` | flag | False | Print the digest text only (no header). |
| `--json` | — | flag | False | Output one JSON object: `{week_start, week_end, entry_count, digest}`. |

**Notes:**

- Without `--llm`, nothing leaves the machine and no model needs to be installed.
- `--llm` (or `DEVLOG_AI=1`) requires a running Ollama server; when it cannot be reached, devlog prints an actionable error and exits 1.
- An empty week prints an info line and exits 0.

**Examples:**

```bash
devlog digest --week                     # extractive, offline
devlog digest --week --llm               # local LLM-written summary
devlog digest --week --llm --model gemma3:12b
devlog digest --week --json | jq -r .digest
```

---

### `devlog ask`

Search the journal **semantically** using local embeddings: `devlog ask "what did I do on auth?"` finds entries by meaning, not just keywords. On first run the whole journal is embedded once (via local Ollama) and cached in `<data-dir>/embeddings.json`; later runs only embed new entries and the query.

```
devlog ask "QUERY" [OPTIONS]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--top` | `-n` | int | 5 | Max matches to show. |
| `--json` | — | flag | False | Output `{"query", "results": [{id, message, created_at, score}, …]}` sorted by descending score. |
| `--model` | — | string | `nomic-embed-text` | Embedding model. Changing it (or `DEVLOG_EMBED_MODEL`) triggers a one-time index rebuild. |

**Notes:**

- Requires a running Ollama server (the query and any new entries are embedded per call).
- The index is storage-backend-agnostic: it lives in its own file next to the journal, so it works identically on the `json` and `sqlite` backends.
- Stale index entries (ids no longer in the journal) are pruned automatically.

**Examples:**

```bash
devlog ask "what did I do on auth?"
devlog ask "database migration" --json | jq '.results[0]'
devlog ask "deployments" --top 3
```

---

### `devlog mcp`

Expose the journal to AI agents over the [Model Context Protocol](https://modelcontextprotocol.io): newline-delimited JSON-RPC 2.0 on stdio, implemented with the Python stdlib only. Tools: `list_entries` (optional `tag` filter and `limit`), `search_entries` (case-insensitive substring), `add_entry` (`message` required, `tags` optional), and `get_stats` (totals, date range, top tags). Tool failures use the MCP `isError` convention — the server loop never crashes.

```
devlog mcp [--check]
```

| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--check` | — | flag | False | Print server info as JSON and exit (sanity check). |

**Example `mcpServers` block** (Claude Desktop, Hermes, OpenCode, Continue.dev):

```json
{
  "mcpServers": {
    "devlog": {
      "command": "devlog",
      "args": ["mcp"]
    }
  }
}
```

**Example session:**

```bash
$ echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_stats","arguments":{}}}' | devlog mcp
{"jsonrpc": "2.0", "id": 1, "result": {"content": [{"type": "text", "text": "{\"total\": 42, ...}"}], "isError": false}}
```

---

## Tag rules

Every command that accepts tags (`add`, `edit`, `tag`, `rename-tag`,
`merge-tag`, plus the `--tag` filter on `list` / `search` / `tail` /
`export`) applies the same rules:

- **Character set:** `^[a-z0-9-]+$` — lowercase letters, digits, and hyphens. Other characters (uppercase, underscores, spaces, punctuation) are rejected.
- **Length:** ≤ 32 characters per tag.
- **Count:** ≤ 10 distinct tags per entry.
- **Normalisation:** whitespace is stripped and the result is lowercased before storage, so `  Backend ` is stored as `backend`. Duplicate tags on the same entry are silently deduplicated.
- **Storage:** tags are stored verbatim (already normalised) and rendered as a comma-separated list in tables and panels.

These rules are enforced at every boundary; a tag that violates them is rejected with a clear error and the command exits with code 1 before any file I/O happens.

---

## Date filters

`devlog list`, `devlog search`, `devlog export`, and `devlog stats` all accept `--since` and `--until` flags. Bounds are inclusive: `--until 2025-01-15` includes entries written on January 15th.

| Format | Meaning |
|--------|---------|
| `YYYY-MM-DD` | That date at 00:00 UTC. As an upper bound, includes the whole day. |
| `YYYY-MM-DDTHH:MM` | ISO 8601, treated as UTC if no offset. |
| `YYYY-MM-DDTHH:MM:SSZ` | ISO 8601 with explicit UTC. |
| `YYYY-MM-DD HH:MM` | Space-separated form, same semantics as `T`. |
| `today` | Today at 00:00 UTC. |
| `yesterday` | Yesterday at 00:00 UTC. |
| `Nd` | N days ago at 00:00 UTC (e.g. `7d`, `30d`). |
| `Nw` | N weeks ago at 00:00 UTC (e.g. `1w`, `2w`). |

Both bounds may be combined. Entries with unparseable timestamps are silently dropped when any bound is set, since a date filter is meaningless for them.

**Examples:**

```bash
# Everything logged in 2025
devlog list --since 2025-01-01 --until 2025-12-31

# Last 7 days, as JSON
devlog list --since 7d --quiet

# Stats for Q1
devlog stats --since 2025-01-01 --until 2025-03-31

# Export last month
devlog export --since 30d -o month.md
```

---

## Configuration

### `DEVLOG_DATA_DIR`

By default, devlog stores all entries at:

```
~/.devlog/entries.json
```

To store entries in a different location — for example, inside a synced folder or a project directory — set the `DEVLOG_DATA_DIR` environment variable. devlog will read and write `entries.json` inside whatever directory you specify.

```bash
# Store entries in a Dropbox-synced folder
export DEVLOG_DATA_DIR="$HOME/Dropbox/devlog"
devlog add "Entry goes here"
# → written to ~/Dropbox/devlog/entries.json

# Store entries in a project-local directory
export DEVLOG_DATA_DIR="$(pwd)/.devlog"
devlog list
```

The target directory is created automatically on first run if it does not exist.

You can make the variable permanent by adding it to your shell configuration:

```bash
# ~/.zshrc or ~/.bashrc
export DEVLOG_DATA_DIR="$HOME/Dropbox/devlog"
```

### `DEVLOG_BACKEND`

Select the storage backend: `json` (default) or `sqlite`.

```bash
export DEVLOG_BACKEND=sqlite
devlog add "hello from SQLite"
```

| | `json` (default) | `sqlite` |
|---|---|---|
| Store | `<data-dir>/entries.json` | `<data-dir>/entries.db` |
| Write pattern | Full-file rewrite (atomic tmp + `os.replace`) | Single-row INSERT/UPDATE/DELETE in a transaction |
| Concurrency | Cross-process `flock`/`msvcrt` locking | WAL journal mode + 5s busy timeout |
| Sweet spot | Journals up to ~10k entries | 10k+ entries, multiple simultaneous processes |
| Corrupt-file recovery | `repair` right-truncation recovery | Not applicable (live database with its own crash safety) |

**Notes:**

- Both backends implement the same command surface; switch by changing the env var — no migration command needed.
- `backup` always writes a JSON snapshot regardless of backend, so `backup` → switch backend → `restore` migrates a journal between backends:

```bash
# Migrate JSON → SQLite
DEVLOG_BACKEND=json devlog backup -q            # prints the snapshot path
export DEVLOG_BACKEND=sqlite
devlog restore <path-from-above> -y
```

- `doctor` and `repair` are backend-aware and report the active store's path and health.

### `DEVLOG_TZ`

Set an IANA time-zone name (e.g. `America/New_York`, `Europe/Berlin`) to make time-bucketed commands reflect your wall clock instead of UTC. When set:

- `today`, `yesterday`, `week`, `stats`, and `calendar` bucket entries by **local** date.
- `--since` / `--until` and `Nd` / `Nw` are interpreted at **local midnight** and converted to UTC for the comparison.
- `stats` renders the `First` / `Last` / `Span` lines with the zone's key suffix (e.g. `… EST`) instead of `… UTC`.
- `add --at "2025-08-28 09:00"` and `edit --at "2025-08-28 09:00"` (no offset) are interpreted in this zone.

On-disk timestamps are always UTC, regardless of this setting.

```bash
export DEVLOG_TZ="America/New_York"
devlog today          # → entries from today in New York
devlog stats          # → date range rendered in EST
devlog list --since 7d   # → last 7 local days
```

A bad zone name surfaces a red error and exit 1, never a silent fallback to UTC:

```bash
$ DEVLOG_TZ="Not/A_Real_Zone" devlog today
Error: Invalid DEVLOG_TZ "Not/A_Real_Zone": …
```

### `DEVLOG_AI`, `DEVLOG_OLLAMA_HOST`, `DEVLOG_EMBED_MODEL`

Control the AI tier (`digest --llm`, `ask`). All three have working defaults; nothing here is required for the offline extractive digest.

| Variable | Default | Purpose |
|----------|---------|---------|
| `DEVLOG_AI` | unset | Set to `1` to make `digest --week` produce an LLM summary by default (same as passing `--llm` each time). |
| `DEVLOG_OLLAMA_HOST` | `http://127.0.0.1:11434` | Ollama server URL — point it at another machine on your LAN (e.g. `http://odysseus:11434`) if the model does not run locally. |
| `DEVLOG_EMBED_MODEL` | `nomic-embed-text` | Embedding model for `ask`. Changing it triggers a one-time index rebuild. |

```bash
# Local LLM summary using the machine on your desk
export DEVLOG_AI=1
export DEVLOG_OLLAMA_HOST="http://odysseus:11434"
devlog digest --week
```

**Model requirements:** `digest --llm` needs a generation model (default `llama3.2`; the summary quality scales with the model, `gemma3:12b` works well). `ask` needs an embedding model (default `nomic-embed-text`). Pull both with `ollama pull llama3.2 nomic-embed-text`.

### `DEVLOG_INTERACTIVE_FORCE`

Set to `1` to bypass the TTY check that normally gates `devlog --interactive`. Useful in tests, scripts, or remote shells. The check still applies to `devlog edit <id>` with no flags unless `DEVLOG_ALLOW_EDITOR_IN_PIPE=1` is also set.

### `DEVLOG_ALLOW_EDITOR_IN_PIPE`

Set to `1` to allow `devlog edit <id>` with no flags to spawn `$EDITOR` even when stdin is not a TTY. The default is to error out, since editors like `nano`/`vi` print screen-clear sequences that pollute captured output. Set this when scripting edits via the editor.

### Themes

devlog's colors are themable. A user theme lives at `~/.devlog/theme.toml` (or `$DEVLOG_DATA_DIR/theme.toml` when set) and overrides individual color roles. Roles you omit use the built-in defaults, so the file is always optional.

```bash
# See the path to your active theme file
devlog theme path
# → /Users/you/.devlog/theme.toml

# See every role and its current value
devlog theme list

# See a single role
devlog theme show date
# → cyan

# Dump a starter theme.toml to STDOUT (all roles commented out, safe template)
devlog theme show > my-theme.toml

# Install a theme file
devlog theme set ~/dotfiles/devlog-theme.toml
# → Theme installed at /Users/you/.devlog/theme.toml (17 roles).
```

The file format is standard TOML with one `[palette]` section. Each key is a *role* (the UI element to color) and each value is a [Rich style string](https://rich.readthedocs.io/en/stable/appendix/colors.html) — a named color, a hex code, a 256-color index, or a composite like `"bold yellow"`.

```toml
# ~/.devlog/theme.toml
[palette]
date             = "bright_cyan"   # default: "cyan"
tags             = "white"         # default: "magenta"
success_border   = "bright_green"  # default: "green"
match_highlight  = "bold magenta"  # default: "bold yellow"
id_dim           = "grey50"        # default: "dim white"
```

The full list of roles:

| Role | Used for | Default |
|---|---|---|
| `error_border` | red panel border on errors | `red` |
| `error_text` | red error text and ✘ icon | `red` |
| `warning_text` | yellow warning text and ⚠ icon | `yellow` |
| `info_text` | dim ℹ info line | `dim` |
| `success_border` | green border on `add` success panel | `green` |
| `success_text` | green body text on ✔ confirmation lines | `green` |
| `success_title` | green title + ✔ icon on `add` success | `bold green` |
| `show_border` | cyan border on `show` and stats panels | `cyan` |
| `delete_border` | red border + ✘ on `delete` | `red` |
| `edit_border` | blue border + ✎ on `edit` | `blue` |
| `date` | cyan date cells in tables and panels | `cyan` |
| `updated` | yellow "Updated" cells | `yellow` |
| `tags` | magenta tag cells | `magenta` |
| `id_dim` | dim short-id cells | `dim white` |
| `match_highlight` | yellow search-match highlight | `bold yellow` |
| `banner_version` | version number on `--version` | `bold cyan` |
| `banner_command` | command names in root help banner | `bold cyan` |
| `prompt_border` | interactive REPL prompt | `magenta` |
| `table_caption` | dim caption under a table | `dim` |
| `table_footer` | bold footer under a table | `bold` |
| `sparkline` | 30-day histogram in `stats` | `cyan` |
| `zebra_alt` | alternate-row dim style in `list` | `dim` |
| `heatmap_base` | base hue for the calendar heatmap | `green` |
| `heatmap_empty` | empty cells in the `calendar` heatmap | `grey15` |
| `heatmap_l1` | lightest non-zero tier in the heatmap | `green` |
| `heatmap_l2` | second heatmap tier | `color(34)` |
| `heatmap_l3` | third heatmap tier | `color(40)` |
| `heatmap_l4` | busiest day in the heatmap | `color(46)` |

Notes:

- Box styles, icons, and layout are not themable in this version. The theming layer is intentionally limited to colors.
- A malformed `theme.toml` is non-fatal: devlog prints one warning to STDERR (`Warning: theme file at … is invalid; using default theme.`) and renders with the defaults.
- Unknown role keys are dropped with a warning per key.
- `NO_COLOR=1` and non-TTY output always suppress color regardless of the theme.
- Changes take effect on the next devlog invocation; no daemon or reload needed.

---

## Development Setup

```bash
# 1. Clone the repository
git clone https://github.com/SpIob/devlog-cli.git
cd devlog-cli

# 2. Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate

# 3. Install in editable mode with development dependencies
pip install -e .
pip install pytest

# 4. Verify the installation
devlog --version

# 5. Run the test suite
pytest
```

**Project layout:**

```
devlog-cli/
├── devlog/
│   ├── __init__.py
│   ├── cli.py            ← Click entry point, all command definitions
│   ├── ui.py             ← Rich rendering helpers (panels, tables, errors)
│   ├── themes.py         ← Theme loader, role contract, palette helpers
│   ├── theme_preview.py  ← Synthetic UI fixture for theme create previews
│   ├── storage.py        ← File I/O, JSON read/write, atomic write logic
│   ├── models.py         ← Entry dataclass / TypedDict definition
│   ├── _completions.py   ← Bash / zsh / fish completion script generators
│   ├── _dates.py         ← Date-range parsing, week boundaries, TZ helpers
│   ├── _filter.py        ← Shared filter chain (tag → date → sort)
│   ├── _interactive.py   ← Line-based REPL dispatcher
│   ├── _io.py            ← Export/import readers and writers
│   ├── _iso.py           ← ISO-8601 parsing / formatting helpers
│   ├── _tagops.py        ← Tag normalisation, validation, merge helpers
│   ├── _git_cli.py       ← `devlog git` command group (status/sync/log)
│   ├── _gitops.py        ← Git plumbing for the git command group
│   ├── _aiops.py         ← Pure AI core: extractive digest, cosine ranking, answer formatting
│   ├── _ollama.py        ← Minimal stdlib Ollama HTTP client (generate/embed)
│   ├── _aiindex.py       ← Embedding index for `ask` (build/reuse/prune, embeddings.json)
│   ├── _ai_cli.py        ← `devlog digest` and `devlog ask` commands
│   ├── _mcp_server.py    ← MCP protocol + journal tools (stdlib JSON-RPC over stdio)
│   ├── _mcp_cli.py       ← `devlog mcp` command
│   └── builtins/         ← Bundled theme files (default, dracula, nord, …)
│       ├── __init__.py
│       ├── default.toml
│       ├── dracula.toml
│       ├── gruvbox.toml
│       ├── high_contrast.toml
│       ├── monokai.toml
│       ├── nord.toml
│       ├── one_dark.toml
│       └── solarized.toml
├── tests/
│   ├── conftest.py
│   ├── test_add.py
│   ├── test_ai_cli.py
│   ├── test_aiindex.py
│   ├── test_aiops.py
│   ├── test_backup_restore.py
│   ├── test_calendar.py
│   ├── test_completions.py
│   ├── test_date_range.py
│   ├── test_delete.py
│   ├── test_doctor.py
│   ├── test_edit.py
│   ├── test_export.py
│   ├── test_import.py
│   ├── test_interactive.py
│   ├── test_limit_validation.py
│   ├── test_list.py
│   ├── test_merge_tag.py
│   ├── test_mcp_server.py
│   ├── test_ollama.py
│   ├── test_performance.py
│   ├── test_rename_tag.py
│   ├── test_repair.py
│   ├── test_search.py
│   ├── test_show.py
│   ├── test_tag_command.py
│   ├── test_tags.py
│   ├── test_themes.py
│   ├── test_timezone.py
│   ├── test_today_tail_stats.py
│   ├── test_ui_columns.py
│   ├── test_ui.py
│   └── test_week_yesterday.py
├── pyproject.toml
└── README.md
```

Tests use Click's `CliRunner` for full end-to-end command testing without touching the real filesystem. Each command has a happy-path test, an empty-state test, and at least one error-condition test.

---

## License

MIT License

Copyright (c) 2025

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
