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.
- liametahi-0.1.0/.github/workflows/ci.yml +40 -0
- liametahi-0.1.0/.github/workflows/release.yml +35 -0
- liametahi-0.1.0/.gitignore +33 -0
- liametahi-0.1.0/.python-version +1 -0
- liametahi-0.1.0/AGENTS.md +102 -0
- liametahi-0.1.0/LICENSE +21 -0
- liametahi-0.1.0/PKG-INFO +113 -0
- liametahi-0.1.0/README.md +89 -0
- liametahi-0.1.0/config.example.yaml +85 -0
- liametahi-0.1.0/docs/configuration.md +179 -0
- liametahi-0.1.0/docs/development.md +84 -0
- liametahi-0.1.0/docs/internals.md +184 -0
- liametahi-0.1.0/pyproject.toml +77 -0
- liametahi-0.1.0/src/liametahi/__init__.py +4 -0
- liametahi-0.1.0/src/liametahi/__main__.py +6 -0
- liametahi-0.1.0/src/liametahi/backup.py +400 -0
- liametahi-0.1.0/src/liametahi/classifier/__init__.py +117 -0
- liametahi-0.1.0/src/liametahi/classifier/anthropic.py +180 -0
- liametahi-0.1.0/src/liametahi/classifier/jev.py +293 -0
- liametahi-0.1.0/src/liametahi/classifier/openai_compatible.py +210 -0
- liametahi-0.1.0/src/liametahi/cli.py +474 -0
- liametahi-0.1.0/src/liametahi/config.py +1072 -0
- liametahi-0.1.0/src/liametahi/domain.py +72 -0
- liametahi-0.1.0/src/liametahi/evaluate.py +931 -0
- liametahi-0.1.0/src/liametahi/execute.py +777 -0
- liametahi-0.1.0/src/liametahi/imap_adapter.py +1010 -0
- liametahi-0.1.0/src/liametahi/locks.py +100 -0
- liametahi-0.1.0/src/liametahi/logging.py +157 -0
- liametahi-0.1.0/src/liametahi/migrations/0001_initial.sql +193 -0
- liametahi-0.1.0/src/liametahi/policy.py +223 -0
- liametahi-0.1.0/src/liametahi/progress.py +189 -0
- liametahi-0.1.0/src/liametahi/prompt.py +523 -0
- liametahi-0.1.0/src/liametahi/py.typed +0 -0
- liametahi-0.1.0/src/liametahi/report.py +405 -0
- liametahi-0.1.0/src/liametahi/rules.py +501 -0
- liametahi-0.1.0/src/liametahi/runner.py +1177 -0
- liametahi-0.1.0/src/liametahi/state.py +1078 -0
- liametahi-0.1.0/tests/__init__.py +0 -0
- liametahi-0.1.0/tests/conftest.py +119 -0
- liametahi-0.1.0/tests/corpus/synthetic/manifest.json +75 -0
- liametahi-0.1.0/tests/corpus/synthetic/messages/4339ef0e48eedc50a1d298a272b92bc3d24bd44fddcb03de6483fb76394ca7a5.eml +20 -0
- liametahi-0.1.0/tests/corpus/synthetic/messages/52afbcc587cf1945bb9783ab3c3c58c80fa396bdc4593c2adc1ab90f5ec81890.eml +9 -0
- liametahi-0.1.0/tests/corpus/synthetic/messages/7d9614583bd6133e6d12416a815d3d331948b326ecbf7c2dcf28191253679782.eml +16 -0
- liametahi-0.1.0/tests/corpus/synthetic/messages/982090a92aa85c3136b36447079d24a7f4673745c9e0e43d434b38c733e215a4.eml +11 -0
- liametahi-0.1.0/tests/corpus/synthetic/messages/cb3e80e64dbc511a98402dd2d86965209c2544564b2011be7db2d281a10587da.eml +10 -0
- liametahi-0.1.0/tests/corpus/synthetic/messages/f275f45a6ae373b01ae37312010f6ac71c9b0e1b3e8e16cbdb7aee2ad9d18bc0.eml +8 -0
- liametahi-0.1.0/tests/fakes/__init__.py +2 -0
- liametahi-0.1.0/tests/fakes/fake_classifier.py +171 -0
- liametahi-0.1.0/tests/fakes/fake_mailbox.py +340 -0
- liametahi-0.1.0/tests/integration/__init__.py +5 -0
- liametahi-0.1.0/tests/integration/conftest.py +290 -0
- liametahi-0.1.0/tests/integration/test_acceptance_11_seen_flag.py +58 -0
- liametahi-0.1.0/tests/integration/test_dovecot_append_and_seed.py +104 -0
- liametahi-0.1.0/tests/integration/test_dovecot_bodystructure.py +103 -0
- liametahi-0.1.0/tests/integration/test_dovecot_capabilities.py +39 -0
- liametahi-0.1.0/tests/integration/test_dovecot_combined_fetch.py +133 -0
- liametahi-0.1.0/tests/integration/test_dovecot_keywords.py +33 -0
- liametahi-0.1.0/tests/integration/test_dovecot_move_and_uidvalidity.py +82 -0
- liametahi-0.1.0/tests/test_acceptance.py +589 -0
- liametahi-0.1.0/tests/test_acceptance_classify.py +257 -0
- liametahi-0.1.0/tests/test_acceptance_mailbox.py +534 -0
- liametahi-0.1.0/tests/test_backup.py +331 -0
- liametahi-0.1.0/tests/test_body_excerpt.py +191 -0
- liametahi-0.1.0/tests/test_candidate_lifecycle.py +392 -0
- liametahi-0.1.0/tests/test_capture_corpus.py +254 -0
- liametahi-0.1.0/tests/test_classifier_anthropic.py +288 -0
- liametahi-0.1.0/tests/test_classifier_jev.py +371 -0
- liametahi-0.1.0/tests/test_classifier_openai_compatible.py +334 -0
- liametahi-0.1.0/tests/test_cli.py +429 -0
- liametahi-0.1.0/tests/test_config.py +1495 -0
- liametahi-0.1.0/tests/test_decision_cache_retry.py +120 -0
- liametahi-0.1.0/tests/test_domain.py +114 -0
- liametahi-0.1.0/tests/test_evaluate.py +955 -0
- liametahi-0.1.0/tests/test_evaluate_concurrency.py +464 -0
- liametahi-0.1.0/tests/test_execute.py +913 -0
- liametahi-0.1.0/tests/test_execute_round_trips.py +259 -0
- liametahi-0.1.0/tests/test_fakes.py +289 -0
- liametahi-0.1.0/tests/test_imap_adapter.py +282 -0
- liametahi-0.1.0/tests/test_locks.py +139 -0
- liametahi-0.1.0/tests/test_logging.py +115 -0
- liametahi-0.1.0/tests/test_policy.py +178 -0
- liametahi-0.1.0/tests/test_progress.py +290 -0
- liametahi-0.1.0/tests/test_prompt.py +644 -0
- liametahi-0.1.0/tests/test_prompt_injection.py +243 -0
- liametahi-0.1.0/tests/test_report.py +429 -0
- liametahi-0.1.0/tests/test_rules.py +703 -0
- liametahi-0.1.0/tests/test_runner.py +162 -0
- liametahi-0.1.0/tests/test_runner_unwritable_paths.py +97 -0
- liametahi-0.1.0/tests/test_state.py +908 -0
- liametahi-0.1.0/tests/test_transaction_batching.py +218 -0
- liametahi-0.1.0/tools/__init__.py +2 -0
- liametahi-0.1.0/tools/capture_corpus.py +398 -0
- liametahi-0.1.0/tools/dev_imap.py +289 -0
- 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.
|
liametahi-0.1.0/LICENSE
ADDED
|
@@ -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.
|
liametahi-0.1.0/PKG-INFO
ADDED
|
@@ -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]
|