liametahi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. liametahi-0.1.0/.github/workflows/ci.yml +40 -0
  2. liametahi-0.1.0/.github/workflows/release.yml +35 -0
  3. liametahi-0.1.0/.gitignore +33 -0
  4. liametahi-0.1.0/.python-version +1 -0
  5. liametahi-0.1.0/AGENTS.md +102 -0
  6. liametahi-0.1.0/LICENSE +21 -0
  7. liametahi-0.1.0/PKG-INFO +113 -0
  8. liametahi-0.1.0/README.md +89 -0
  9. liametahi-0.1.0/config.example.yaml +85 -0
  10. liametahi-0.1.0/docs/configuration.md +179 -0
  11. liametahi-0.1.0/docs/development.md +84 -0
  12. liametahi-0.1.0/docs/internals.md +184 -0
  13. liametahi-0.1.0/pyproject.toml +77 -0
  14. liametahi-0.1.0/src/liametahi/__init__.py +4 -0
  15. liametahi-0.1.0/src/liametahi/__main__.py +6 -0
  16. liametahi-0.1.0/src/liametahi/backup.py +400 -0
  17. liametahi-0.1.0/src/liametahi/classifier/__init__.py +117 -0
  18. liametahi-0.1.0/src/liametahi/classifier/anthropic.py +180 -0
  19. liametahi-0.1.0/src/liametahi/classifier/jev.py +293 -0
  20. liametahi-0.1.0/src/liametahi/classifier/openai_compatible.py +210 -0
  21. liametahi-0.1.0/src/liametahi/cli.py +474 -0
  22. liametahi-0.1.0/src/liametahi/config.py +1072 -0
  23. liametahi-0.1.0/src/liametahi/domain.py +72 -0
  24. liametahi-0.1.0/src/liametahi/evaluate.py +931 -0
  25. liametahi-0.1.0/src/liametahi/execute.py +777 -0
  26. liametahi-0.1.0/src/liametahi/imap_adapter.py +1010 -0
  27. liametahi-0.1.0/src/liametahi/locks.py +100 -0
  28. liametahi-0.1.0/src/liametahi/logging.py +157 -0
  29. liametahi-0.1.0/src/liametahi/migrations/0001_initial.sql +193 -0
  30. liametahi-0.1.0/src/liametahi/policy.py +223 -0
  31. liametahi-0.1.0/src/liametahi/progress.py +189 -0
  32. liametahi-0.1.0/src/liametahi/prompt.py +523 -0
  33. liametahi-0.1.0/src/liametahi/py.typed +0 -0
  34. liametahi-0.1.0/src/liametahi/report.py +405 -0
  35. liametahi-0.1.0/src/liametahi/rules.py +501 -0
  36. liametahi-0.1.0/src/liametahi/runner.py +1177 -0
  37. liametahi-0.1.0/src/liametahi/state.py +1078 -0
  38. liametahi-0.1.0/tests/__init__.py +0 -0
  39. liametahi-0.1.0/tests/conftest.py +119 -0
  40. liametahi-0.1.0/tests/corpus/synthetic/manifest.json +75 -0
  41. liametahi-0.1.0/tests/corpus/synthetic/messages/4339ef0e48eedc50a1d298a272b92bc3d24bd44fddcb03de6483fb76394ca7a5.eml +20 -0
  42. liametahi-0.1.0/tests/corpus/synthetic/messages/52afbcc587cf1945bb9783ab3c3c58c80fa396bdc4593c2adc1ab90f5ec81890.eml +9 -0
  43. liametahi-0.1.0/tests/corpus/synthetic/messages/7d9614583bd6133e6d12416a815d3d331948b326ecbf7c2dcf28191253679782.eml +16 -0
  44. liametahi-0.1.0/tests/corpus/synthetic/messages/982090a92aa85c3136b36447079d24a7f4673745c9e0e43d434b38c733e215a4.eml +11 -0
  45. liametahi-0.1.0/tests/corpus/synthetic/messages/cb3e80e64dbc511a98402dd2d86965209c2544564b2011be7db2d281a10587da.eml +10 -0
  46. liametahi-0.1.0/tests/corpus/synthetic/messages/f275f45a6ae373b01ae37312010f6ac71c9b0e1b3e8e16cbdb7aee2ad9d18bc0.eml +8 -0
  47. liametahi-0.1.0/tests/fakes/__init__.py +2 -0
  48. liametahi-0.1.0/tests/fakes/fake_classifier.py +171 -0
  49. liametahi-0.1.0/tests/fakes/fake_mailbox.py +340 -0
  50. liametahi-0.1.0/tests/integration/__init__.py +5 -0
  51. liametahi-0.1.0/tests/integration/conftest.py +290 -0
  52. liametahi-0.1.0/tests/integration/test_acceptance_11_seen_flag.py +58 -0
  53. liametahi-0.1.0/tests/integration/test_dovecot_append_and_seed.py +104 -0
  54. liametahi-0.1.0/tests/integration/test_dovecot_bodystructure.py +103 -0
  55. liametahi-0.1.0/tests/integration/test_dovecot_capabilities.py +39 -0
  56. liametahi-0.1.0/tests/integration/test_dovecot_combined_fetch.py +133 -0
  57. liametahi-0.1.0/tests/integration/test_dovecot_keywords.py +33 -0
  58. liametahi-0.1.0/tests/integration/test_dovecot_move_and_uidvalidity.py +82 -0
  59. liametahi-0.1.0/tests/test_acceptance.py +589 -0
  60. liametahi-0.1.0/tests/test_acceptance_classify.py +257 -0
  61. liametahi-0.1.0/tests/test_acceptance_mailbox.py +534 -0
  62. liametahi-0.1.0/tests/test_backup.py +331 -0
  63. liametahi-0.1.0/tests/test_body_excerpt.py +191 -0
  64. liametahi-0.1.0/tests/test_candidate_lifecycle.py +392 -0
  65. liametahi-0.1.0/tests/test_capture_corpus.py +254 -0
  66. liametahi-0.1.0/tests/test_classifier_anthropic.py +288 -0
  67. liametahi-0.1.0/tests/test_classifier_jev.py +371 -0
  68. liametahi-0.1.0/tests/test_classifier_openai_compatible.py +334 -0
  69. liametahi-0.1.0/tests/test_cli.py +429 -0
  70. liametahi-0.1.0/tests/test_config.py +1495 -0
  71. liametahi-0.1.0/tests/test_decision_cache_retry.py +120 -0
  72. liametahi-0.1.0/tests/test_domain.py +114 -0
  73. liametahi-0.1.0/tests/test_evaluate.py +955 -0
  74. liametahi-0.1.0/tests/test_evaluate_concurrency.py +464 -0
  75. liametahi-0.1.0/tests/test_execute.py +913 -0
  76. liametahi-0.1.0/tests/test_execute_round_trips.py +259 -0
  77. liametahi-0.1.0/tests/test_fakes.py +289 -0
  78. liametahi-0.1.0/tests/test_imap_adapter.py +282 -0
  79. liametahi-0.1.0/tests/test_locks.py +139 -0
  80. liametahi-0.1.0/tests/test_logging.py +115 -0
  81. liametahi-0.1.0/tests/test_policy.py +178 -0
  82. liametahi-0.1.0/tests/test_progress.py +290 -0
  83. liametahi-0.1.0/tests/test_prompt.py +644 -0
  84. liametahi-0.1.0/tests/test_prompt_injection.py +243 -0
  85. liametahi-0.1.0/tests/test_report.py +429 -0
  86. liametahi-0.1.0/tests/test_rules.py +703 -0
  87. liametahi-0.1.0/tests/test_runner.py +162 -0
  88. liametahi-0.1.0/tests/test_runner_unwritable_paths.py +97 -0
  89. liametahi-0.1.0/tests/test_state.py +908 -0
  90. liametahi-0.1.0/tests/test_transaction_batching.py +218 -0
  91. liametahi-0.1.0/tools/__init__.py +2 -0
  92. liametahi-0.1.0/tools/capture_corpus.py +398 -0
  93. liametahi-0.1.0/tools/dev_imap.py +289 -0
  94. liametahi-0.1.0/uv.lock +658 -0
@@ -0,0 +1,40 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ workflow_call:
8
+
9
+ jobs:
10
+ gate:
11
+ # implementation-contracts.md §8: definition of done for every unit.
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Install uv
17
+ uses: astral-sh/setup-uv@v3
18
+ with:
19
+ enable-cache: true
20
+
21
+ - name: Install Python (per pyproject requires-python)
22
+ run: uv python install
23
+
24
+ - name: Install dependencies
25
+ run: uv sync --all-groups
26
+
27
+ - name: ruff format --check
28
+ run: uv run ruff format --check .
29
+
30
+ - name: ruff check
31
+ run: uv run ruff check .
32
+
33
+ - name: mypy --strict (src)
34
+ run: uv run mypy --strict src/liametahi
35
+
36
+ - name: mypy --strict (tests)
37
+ run: uv run mypy --strict tests
38
+
39
+ - name: pytest (unit tier; integration skipped without Docker, live never runs)
40
+ run: uv run pytest -q
@@ -0,0 +1,35 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ gate:
9
+ uses: ./.github/workflows/ci.yml
10
+
11
+ publish:
12
+ needs: gate
13
+ runs-on: ubuntu-latest
14
+ environment: pypi
15
+ permissions:
16
+ id-token: write
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v3
22
+ with:
23
+ enable-cache: true
24
+
25
+ - name: Build
26
+ run: uv build
27
+
28
+ - name: Smoke-test the built wheel
29
+ run: |
30
+ uv venv /tmp/liametahi-smoke
31
+ uv pip install --python /tmp/liametahi-smoke/bin/python dist/*.whl
32
+ /tmp/liametahi-smoke/bin/liametahi --help
33
+
34
+ - name: Publish to PyPI
35
+ run: uv publish --trusted-publishing automatic
@@ -0,0 +1,33 @@
1
+ # Configuration holds literal mailbox and model credentials (spec §12) — never commit it.
2
+ liametahi.yaml
3
+ liametahi.yml
4
+ *.local.yaml
5
+ config.yaml
6
+
7
+ # Local state and backups
8
+ *.sqlite3
9
+ *.sqlite3-wal
10
+ *.sqlite3-shm
11
+ backups/
12
+
13
+ # Real mail is never a test dependency and is never committed (contracts
14
+ # §6.2). Only the hand-written synthetic corpus is; captured corpora
15
+ # under any other tests/corpus/<name>/ are git-ignored.
16
+ *.eml
17
+ tests/corpus/*/
18
+ !tests/corpus/synthetic/
19
+ !tests/corpus/synthetic/**
20
+
21
+ # Python
22
+ __pycache__/
23
+ *.py[cod]
24
+ .venv/
25
+ venv/
26
+ build/
27
+ dist/
28
+ *.egg-info/
29
+ .pytest_cache/
30
+ .ruff_cache/
31
+ .mypy_cache/
32
+
33
+ data/
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,102 @@
1
+ # AGENTS.md
2
+
3
+ Instructions for any agent (or human) working in this repository. Liametahi
4
+ is a local, cron-friendly Python CLI that cleans up an IMAP mailbox, using a
5
+ model only as a constrained classifier — never as the thing that decides or
6
+ acts. If anything here conflicts with the documents it points to, those
7
+ documents win; this file is a map and a set of reminders, not a new source of
8
+ truth.
9
+
10
+ ## Where things live
11
+
12
+ | Path | What it is | Authoritative for |
13
+ | --- | --- | --- |
14
+ | `README.md` | User-facing install/quickstart/CLI usage | End-user behavior and docs |
15
+ | `docs/configuration.md` | Full `config.yaml` key reference: accounts, models, processors, tasks, rule conditions | Config file shape and defaults |
16
+ | `docs/internals.md` | Expanded reasoning behind each safety-model bullet | Why the safety properties exist |
17
+ | `docs/development.md` | Checks to run, test tiers, live-mailbox testing workflow | Dev workflow |
18
+ | `dev-notes/specification.md` | Full product/behavior spec, numbered acceptance criteria (§14), resolved decisions (§15) | **What** the tool does and why — read this first for any behavior question |
19
+ | `dev-notes/implementation-contracts.md` | Toolchain, SQLite DDL, module signatures, work-unit boundaries, test-gate commands | **Shape** of the implementation — read this first for any "how is this wired together" question |
20
+ | `dev-notes/jev-provider-plan.md` | The `processors`/Jev redesign brief (chat + Jev classification unified, `task:<id>` routing) | Authoritative for that redesign's shape until folded fully into `specification.md` (in progress — §2/§3/§11/§13–§15 of the spec still describe the pre-redesign `llm:`/rule-id model) |
21
+ | `dev-notes/jev-provider-design-history.md` | Alternatives considered and rejected during that redesign, with reasons | Background only — nothing here is current design |
22
+ | `dev-notes/*.md` (others) | Historical/working notes | Background only |
23
+ | `dev-notes/archive/*.md` | Fully superseded docs kept for history only (e.g. `mailbox-cleanup-cli.md`, superseded by `specification.md`) | Not current — do not read for behavior/shape questions |
24
+ | `src/liametahi/` | The package (see module map below) | — |
25
+ | `tests/` | Unit tests (default), integration tests (`-m integration`, needs Docker/Dovecot), live tests (`-m live`, opt-in, real mailbox, never in CI) | — |
26
+ | `tools/` | `capture_corpus.py` (pull a real mailbox into a local synthetic corpus, read-only) and `dev_imap.py` (disposable local Dovecot for integration/live testing) | — |
27
+ | `config.example.yaml` | Template a user copies to `~/.config/liametahi/config.yaml` or `./config.yaml` | Quickstart |
28
+
29
+ When `dev-notes/specification.md` and `dev-notes/implementation-contracts.md`
30
+ disagree: the specification wins on behavior, the contracts document wins on
31
+ shape (stated explicitly at the top of the contracts file).
32
+
33
+ ## Module map (`src/liametahi/`)
34
+
35
+ - `cli.py` — Typer CLI: argument parsing, config path resolution, exit codes,
36
+ printing. Delegates every decision of substance elsewhere.
37
+ - `config.py` — Pydantic v2 models, YAML load-time validation, config file
38
+ ownership/permission checks, condition-tree (`when:`) grammar parsing.
39
+ - `rules.py` / `policy.py` — condition tree types (including `processor:`
40
+ atoms and the `all`/`any`/`none`/`not` composition keywords) and
41
+ rule-matching ("winner takes all" by plain list order — rules have no id
42
+ or priority field; the first matching rule wins) semantics.
43
+ - `imap_adapter.py` — IMAP protocol wrapper (fetch, claim, mutate).
44
+ - `classifier/` — `anthropic.py`, `openai_compatible.py`, and `jev.py`
45
+ adapters behind a shared interface in `__init__.py`. A model only ever
46
+ answers a named **processor**'s declared question (`noul`/`choice`/
47
+ `score`) against its own closed vocabulary of options/levels — never
48
+ free-form, never widening what a rule may act on. See
49
+ `dev-notes/jev-provider-plan.md` for the full design.
50
+ - `evaluate.py` / `execute.py` — the two run phases: evaluate (classify,
51
+ decide, cache) and execute (backup, mutate).
52
+ - `runner.py` — orchestrates a full task run across both phases, locking,
53
+ and crash recovery.
54
+ - `backup.py` — content-addressed `.eml` backup + restore.
55
+ - `state.py` — SQLite schema/access (see contracts doc for DDL).
56
+ - `locks.py` — per-task advisory locks (`--wait`, exit `5` on contention).
57
+ - `report.py` — renders a stored run without touching the mailbox or model.
58
+ - `progress.py`, `logging.py`, `prompt.py`, `domain.py` — progress UI,
59
+ redacting logger, LLM prompt construction, and core domain types.
60
+
61
+ ## Conventions (binding, not stylistic preference)
62
+
63
+ - **`uv` for everything** — `uv add`, `uv run`, `uv sync`. Never `pip`.
64
+ - **Full type annotations**; `mypy --strict` covers `src/liametahi` *and*
65
+ `tests` (test doubles implement the same protocols as real adapters, so a
66
+ drifted signature is caught there).
67
+ - Full gate before calling anything done (`docs/development.md`):
68
+ ```sh
69
+ uv run ruff format --check .
70
+ uv run ruff check .
71
+ uv run mypy --strict src/liametahi tests
72
+ uv run pytest
73
+ ```
74
+ - **Never commit real credentials, real email content, or a captured
75
+ corpus.** `config.yaml`, `liametahi.yaml`/`.yml`, `*.local.yaml`, `*.eml`,
76
+ and captured `tests/corpus/*/` (other than the hand-written
77
+ `tests/corpus/synthetic/`) are gitignored on purpose — don't work around it.
78
+ - The config file is a secret (spec §12): wrong ownership is a hard failure
79
+ (exit 2); group/world-readable mode bits print a warning but still load.
80
+ - Safety properties from the spec (`BODY.PEEK` everywhere, one remote
81
+ mutation per message, claim-before-mutate, protected senders/flags
82
+ re-checked immediately before mutation, verify-then-act, and `trash`
83
+ always requiring at least one deterministic — non-`processor:` —
84
+ condition) are non-negotiable — implement them exactly even where a
85
+ simpler shape would pass the tests. (Backup-before-trash is *not* in this
86
+ list: it's deliberately no longer required, per
87
+ `dev-notes/jev-provider-plan.md` §8 — `backup` is still available and
88
+ freely composable, just optional.) A model's output is untrusted,
89
+ attacker-influenced input: validate every processor answer against its
90
+ own declared options/levels, never let it widen what an action may do.
91
+
92
+ ## Specialized agents in this repo
93
+
94
+ - `liametahi-impl` (`.claude/agents/liametahi-impl.md`) implements a work
95
+ unit against the spec and contracts docs.
96
+ - `liametahi-verify` (`.claude/agents/liametahi-verify.md`) is a read-only
97
+ auditor that checks implemented code against those same docs.
98
+
99
+ Both were written before `dev-notes/` was renamed from `notes/`; their
100
+ "read these first" paths have been corrected to `dev-notes/specification.md`
101
+ and `dev-notes/implementation-contracts.md`. If you add a new agent for this
102
+ repo, point it at the `dev-notes/` paths directly.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 4piu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.5
2
+ Name: liametahi
3
+ Version: 0.1.0
4
+ Summary: A local, cron-friendly IMAP mailbox cleanup CLI using an LLM as a constrained classifier.
5
+ Project-URL: Homepage, https://github.com/4piu/liametahi
6
+ Project-URL: Repository, https://github.com/4piu/liametahi
7
+ Project-URL: Issues, https://github.com/4piu/liametahi/issues
8
+ Author: 4piu
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Communications :: Email
15
+ Classifier: Topic :: Utilities
16
+ Requires-Python: >=3.14
17
+ Requires-Dist: anthropic>=0.120.2
18
+ Requires-Dist: httpx>=0.28.1
19
+ Requires-Dist: platformdirs>=4.11.0
20
+ Requires-Dist: pydantic>=2.13.4
21
+ Requires-Dist: pyyaml>=6.0.3
22
+ Requires-Dist: typer>=0.27.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Liametahi
26
+
27
+ An AI-powered mailbox cleanup tool for IMAP. Point it at an inbox and it
28
+ can:
29
+
30
+ - Trash, move, label, or route mail using both plain conditions (sender,
31
+ age, size, headers) and an AI's answer to a question you write ("is this
32
+ spam?", "how urgent is this?")
33
+ - Preview any run with `--dry-run`, back up before deleting, and restore
34
+ anytime
35
+ - Run unattended on a schedule — locking, crash recovery, and a report of
36
+ every past run
37
+ - Mix a fast/cheap model with a slower one, only escalating the mail the
38
+ first pass is unsure about
39
+
40
+ ```yaml
41
+ processors:
42
+ vibe-check:
43
+ model: local
44
+ type: noul
45
+ instructions: A newsletter or digest with nothing time-sensitive left in it.
46
+
47
+ tasks:
48
+ inbox-cleanup:
49
+ account: personal
50
+ source_mailboxes: [INBOX]
51
+ rules:
52
+ - when:
53
+ - older-than: 30d
54
+ - processor: "vibe-check.value >= 0.9"
55
+ actions: [backup, trash]
56
+ ```
57
+
58
+ ## Install
59
+
60
+ Needs Python ≥ 3.14, [`uv`](https://docs.astral.sh/uv/), an IMAP account, and
61
+ a model endpoint — an OpenAI-compatible chat API, Anthropic, or a
62
+ Jev-compatible API.
63
+
64
+ ```sh
65
+ uv tool install liametahi
66
+ liametahi --help
67
+ ```
68
+
69
+ Running from a source checkout instead? See
70
+ [docs/development.md](docs/development.md).
71
+
72
+ ## Usage
73
+
74
+ ```
75
+ liametahi config check [--connect]
76
+ liametahi run TASK [--dry-run] [--fail-fast] [--reevaluate] [--wait SECONDS] [--format table|json] [--verbose]
77
+ liametahi report [RUN_ID] [--list] [--task TASK] [--format table|json] [--verbose]
78
+ liametahi restore BACKUP_ID --mailbox MAILBOX [--account NAME] [--dry-run]
79
+ ```
80
+
81
+ `--config PATH` works on every subcommand. Run/backup ids are short strings
82
+ (`4w8wbbs3fs`); type any unambiguous leading prefix (eg. `4w8b`).
83
+
84
+ ## Quickstart
85
+
86
+ ```sh
87
+ curl -o config.yaml https://raw.githubusercontent.com/4piu/liametahi/master/config.example.yaml
88
+ $EDITOR config.yaml # at least an account and a model
89
+ chmod 600 config.yaml
90
+
91
+ liametahi config check --connect
92
+ liametahi run inbox-classify --dry-run --verbose # preview, no mutation
93
+ liametahi run inbox-classify # for real
94
+ liametahi report # review the last run
95
+ liametahi restore 4w8w --mailbox INBOX # undo a trash
96
+ ```
97
+
98
+ See [docs/configuration.md](docs/configuration.md) for every key and how
99
+ conditions/rules compose. Config file location, first match wins:
100
+
101
+ 1. `--config PATH`
102
+ 2. `$LIAMETAHI_CONFIG`
103
+ 3. `$(pwd)/config.yaml`
104
+ 4. `~/.config/liametahi/config.yaml` (Linux); `~/Library/Application Support/liametahi` (macOS); `%LOCALAPPDATA%\liametahi` (Windows)
105
+
106
+ ## Documentation
107
+
108
+ - [docs/configuration.md](docs/configuration.md) — full config key
109
+ reference: accounts, models, processors, tasks, rule conditions.
110
+ - [docs/internals.md](docs/internals.md) — safety model, run phases,
111
+ decision cache, and the reasoning behind each guarantee.
112
+ - [docs/development.md](docs/development.md) — test tiers, running the
113
+ suite, and live-mailbox testing.
@@ -0,0 +1,89 @@
1
+ # Liametahi
2
+
3
+ An AI-powered mailbox cleanup tool for IMAP. Point it at an inbox and it
4
+ can:
5
+
6
+ - Trash, move, label, or route mail using both plain conditions (sender,
7
+ age, size, headers) and an AI's answer to a question you write ("is this
8
+ spam?", "how urgent is this?")
9
+ - Preview any run with `--dry-run`, back up before deleting, and restore
10
+ anytime
11
+ - Run unattended on a schedule — locking, crash recovery, and a report of
12
+ every past run
13
+ - Mix a fast/cheap model with a slower one, only escalating the mail the
14
+ first pass is unsure about
15
+
16
+ ```yaml
17
+ processors:
18
+ vibe-check:
19
+ model: local
20
+ type: noul
21
+ instructions: A newsletter or digest with nothing time-sensitive left in it.
22
+
23
+ tasks:
24
+ inbox-cleanup:
25
+ account: personal
26
+ source_mailboxes: [INBOX]
27
+ rules:
28
+ - when:
29
+ - older-than: 30d
30
+ - processor: "vibe-check.value >= 0.9"
31
+ actions: [backup, trash]
32
+ ```
33
+
34
+ ## Install
35
+
36
+ Needs Python ≥ 3.14, [`uv`](https://docs.astral.sh/uv/), an IMAP account, and
37
+ a model endpoint — an OpenAI-compatible chat API, Anthropic, or a
38
+ Jev-compatible API.
39
+
40
+ ```sh
41
+ uv tool install liametahi
42
+ liametahi --help
43
+ ```
44
+
45
+ Running from a source checkout instead? See
46
+ [docs/development.md](docs/development.md).
47
+
48
+ ## Usage
49
+
50
+ ```
51
+ liametahi config check [--connect]
52
+ liametahi run TASK [--dry-run] [--fail-fast] [--reevaluate] [--wait SECONDS] [--format table|json] [--verbose]
53
+ liametahi report [RUN_ID] [--list] [--task TASK] [--format table|json] [--verbose]
54
+ liametahi restore BACKUP_ID --mailbox MAILBOX [--account NAME] [--dry-run]
55
+ ```
56
+
57
+ `--config PATH` works on every subcommand. Run/backup ids are short strings
58
+ (`4w8wbbs3fs`); type any unambiguous leading prefix (eg. `4w8b`).
59
+
60
+ ## Quickstart
61
+
62
+ ```sh
63
+ curl -o config.yaml https://raw.githubusercontent.com/4piu/liametahi/master/config.example.yaml
64
+ $EDITOR config.yaml # at least an account and a model
65
+ chmod 600 config.yaml
66
+
67
+ liametahi config check --connect
68
+ liametahi run inbox-classify --dry-run --verbose # preview, no mutation
69
+ liametahi run inbox-classify # for real
70
+ liametahi report # review the last run
71
+ liametahi restore 4w8w --mailbox INBOX # undo a trash
72
+ ```
73
+
74
+ See [docs/configuration.md](docs/configuration.md) for every key and how
75
+ conditions/rules compose. Config file location, first match wins:
76
+
77
+ 1. `--config PATH`
78
+ 2. `$LIAMETAHI_CONFIG`
79
+ 3. `$(pwd)/config.yaml`
80
+ 4. `~/.config/liametahi/config.yaml` (Linux); `~/Library/Application Support/liametahi` (macOS); `%LOCALAPPDATA%\liametahi` (Windows)
81
+
82
+ ## Documentation
83
+
84
+ - [docs/configuration.md](docs/configuration.md) — full config key
85
+ reference: accounts, models, processors, tasks, rule conditions.
86
+ - [docs/internals.md](docs/internals.md) — safety model, run phases,
87
+ decision cache, and the reasoning behind each guarantee.
88
+ - [docs/development.md](docs/development.md) — test tiers, running the
89
+ suite, and live-mailbox testing.
@@ -0,0 +1,85 @@
1
+ # Every inbox message is screened by jev; confident spam is trashed, and
2
+ # anything uncertain is handed off to a second task that asks a chat
3
+ # model to look at the body before deciding.
4
+
5
+ version: 1
6
+
7
+ accounts:
8
+ personal:
9
+ host: imap.gmail.com
10
+ port: 993
11
+ username: you@gmail.com
12
+ password: "an app password, not your real one"
13
+ trash_mailbox: "[Gmail]/Trash"
14
+
15
+ models:
16
+ jev-primary:
17
+ provider: jev
18
+ base_url: https://api.typesafe.ai/v1/systemone
19
+ api_key: "${TYPESAFE_API_KEY}"
20
+ model: jev-latest
21
+ mails_per_request: 1
22
+ max_concurrent_requests: 8
23
+
24
+ local:
25
+ provider: openai_compatible
26
+ base_url: http://127.0.0.1:8080/v1/chat/completions
27
+ model: qwen2.5-7b-instruct
28
+
29
+ processors:
30
+ spam-category:
31
+ model: jev-primary
32
+ type: choice
33
+ instructions: "Is this email spam or a personal/legitimate message?"
34
+ criteria:
35
+ spam: "Unsolicited bulk mail, phishing, or scam content"
36
+ personal: "A legitimate message, whether personal or business"
37
+
38
+ spam-review:
39
+ model: local
40
+ type: noul
41
+ include_body: true
42
+ instructions: "Is this message spam: unsolicited bulk mail, phishing, or a scam?"
43
+ criteria:
44
+ "true": "This message is spam: unsolicited bulk mail, phishing, or a scam."
45
+ "false": "This message is a legitimate message that should be kept."
46
+
47
+ tasks:
48
+ inbox-classify:
49
+ account: personal
50
+ source_mailboxes: [INBOX]
51
+ protect:
52
+ flags: ['\Flagged', '\Answered']
53
+ unread: true
54
+ rules:
55
+ # Confident spam: trash outright, no local backup.
56
+ - when:
57
+ - processor: "spam-category.value == spam"
58
+ - processor: "spam-category.confidence >= 0.85"
59
+ - in-mailbox: INBOX
60
+ actions: [trash]
61
+
62
+ # Not confident: hand off to inbox-review for a closer look.
63
+ - when:
64
+ - processor: "spam-category.confidence < 0.85"
65
+ actions: [task:inbox-review]
66
+
67
+ # Digests/newsletters, unless the subject flags them urgent.
68
+ - when:
69
+ - older-than: 30d
70
+ - any:
71
+ - list-id-contains: digest
72
+ - list-id-contains: newsletter
73
+ - not:
74
+ subject-contains: urgent
75
+ actions: [backup, trash]
76
+
77
+ # Reached only via inbox-classify's task: action above -- no mailbox
78
+ # of its own to scan.
79
+ inbox-review:
80
+ account: personal
81
+ rules:
82
+ - when:
83
+ - processor: "spam-review.value >= 0.9"
84
+ - in-mailbox: INBOX
85
+ actions: [backup, trash]