@bobfrankston/rmfmail 1.2.143 → 1.2.144

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.
package/TODO.md CHANGED
@@ -99,13 +99,14 @@ Everything AI-shaped collected here so Bob can think about it on its own terms,
99
99
  |---|---|
100
100
  | **Q103-AI** ❓ | **Rules / extensions engine shape (C39).** Parked 2026-04-23 while Bob experiments with imail. Revisit once imail findings settle. Candidate shapes when it's time: declarative JSONC rules (`if from: foo then move to bar`), TypeScript plugins under `~/.mailx/extensions/`, AI-classified categories (newsletter / priority / action-required), or a hybrid. |
101
101
  | **Q111-AI** ❓ | **Writing assistance.** LanguageTool (API or self-hosted), custom AI via the existing ghost-text path (Ollama / Claude / OpenAI), in-WebView2 native spellcheck only (current), or something else? |
102
- | **Q113-AI** ❓ | **AI-flavored per-message metadata.** The schema shape in Q113 above directly affects where AI-assigned priority / categories / summaries live. If Q113 lands on a separate `message_meta` table the AI tags have a natural home; if it's a JSON column on `messages`, the AI writer mutates the same row. |
102
+ | **Q113-AI** ❓ | **AI-flavored per-message metadata.** The schema shape in Q113 above directly affects where AI-assigned priority / categories / summaries live. If Q113 lands on a separate `message_meta` table the AI tags have a natural home; if it's a JSON column on `messages`, the AI writer mutates the same row. *Update 2026-07-16: C155 answers the cross-app half — categories/signals live ON the message as IMAP keywords with a shared registry; Q113's table-vs-column question now only covers rmfmail-internal rich residue.* |
103
103
 
104
104
  ### AI — Backlog
105
105
 
106
106
  | # | Status | Item |
107
107
  |---|---|---|
108
- | [**C39**](#ext39) ❓ | PARKED | Rules / extensions engine + AI classification. See Q103-AI. |
108
+ | **C155** ❓ | DRAFT | **Annotation registry + imail companion annotator** [M]. Companion app annotates each letter *before* sorting. Two-layer design (2026-07-16 discussion): data plane = IMAP keywords on the message itself (core RFC 3501, portable — Gmail labels / Graph categories are the same concept per provider); vocabulary plane = one shared `annotations.jsonc` registry with MCP-tool-style entries (`description`/`whenToApply` prompt the AI annotator, `sortAction` drives the sorter, `label` feeds UIs). No shared DB needed — IMAP STORE is the concurrency layer; rich residue (dates, summaries) stays out or comes back later as a service-owned sidecar. Design note: `docs/annotation-registry.md`; starter vocabulary: `docs/annotations.jsonc`. Awaiting Bob's think-through — open questions at the end of the design note. Subsumes the cross-app half of Q113/Q113-AI; feeds Q103-AI/C39/P19 (rules engine shape). |
109
+ | [**C39**](#ext39) ❓ | PARKED | Rules / extensions engine + AI classification. See Q103-AI, C155. |
109
110
  | [**P19**](#priority-daily) ❓ | OPEN | Screener — imail rules + AI classifier. Elevated under daily-driver; same classifier question as C39. |
110
111
  | **AI-writing** ❓ | OPEN | Proofread / rewrite / tone-adjust in compose. Currently a Settings toggle that's off; needs the shape from Q111-AI before wiring. |
111
112
  | **AI-extract-contact** | OPEN | Right-click an email → Add contact auto-fills Name / Organization / Phone from the letter body. "In the future" per user 2026-04-23. Depends on a working AI back-end choice from Q111-AI. |
@@ -0,0 +1,74 @@
1
+ # Annotation registry — companion annotator for imail (design sketch)
2
+
3
+ *Status: DRAFT 2026-07-16 — captured from discussion; Bob thinking it over. TODO item C155.*
4
+
5
+ ## Goal
6
+
7
+ A companion app annotates each letter **before** sorting. imail's sorter (and any
8
+ other consumer — rmfmail UI, agents, future tools) reads the annotations and acts.
9
+ The mechanism should be general — not private to imail the way its `ai/` cache is.
10
+
11
+ ## Decision so far: keywords on the message + a shared JSON registry
12
+
13
+ Two-layer design, no shared database required:
14
+
15
+ 1. **Data plane — tags on the message itself.** IMAP keywords (core RFC 3501,
16
+ not Dovecot-specific: any server advertising `\*` in PERMANENTFLAGS). The
17
+ message carries its own annotations; they survive folder moves; IMAP STORE is
18
+ atomic per message so concurrency is the server's problem, already solved.
19
+ Provider mapping for the same concept:
20
+ | Provider | Mechanism |
21
+ |---|---|
22
+ | Dovecot / Cyrus / Fastmail / iCloud / Yahoo IMAP | user keywords (atoms) |
23
+ | Gmail | API labels (Gmail IMAP ignores custom keywords) |
24
+ | Outlook / Graph | categories |
25
+ | JMAP | keywords (unified with IMAP's by design) |
26
+
27
+ 2. **Vocabulary plane — one shared JSON registry** (`annotations.jsonc`, draft
28
+ next to this doc) with MCP-tool-style entries per keyword: `label`,
29
+ `description`, `whenToApply`, `exclusiveGroup`, `setBy`, `sortAction`.
30
+ The registry does triple duty:
31
+ - the AI annotator's prompt is generated from `description` / `whenToApply`
32
+ (same way MCP tool descriptions steer an agent);
33
+ - the sorter maps keyword → action from `sortAction`;
34
+ - UIs (rmfmail chips, Thunderbird tags) get display names from `label`.
35
+ Adding a category = editing one file. No schema migration, no API change.
36
+
37
+ ## Constraints / cautions
38
+
39
+ - **Keyword budget**: Dovecot on maildir caps keywords ~26/mailbox (filename
40
+ letters). Registry stays small and curated; `exclusiveGroup` keeps categories
41
+ mutually exclusive instead of accreting.
42
+ - **Atoms only**: no spaces, ASCII, prefix everything `im_` so the vocabulary
43
+ can't collide with `$Junk`, `$Forwarded`, etc.
44
+ - **Tags, not key-value**: scores get bucketed (`im_spam_high`); anything
45
+ non-enumerable (extracted dates, reasoning text, summaries) does NOT fit.
46
+ If rich values are needed later, a sidecar store comes back *only for that
47
+ residue* — the keyword layer stays as the interoperable spine. (imail's
48
+ `.ics` writer is precedent: rich results can also just be files.)
49
+ - **Registry distribution**: share it the way accounts.jsonc is shared — GDrive
50
+ via API with a local cache, never a file on a network path. Slots into
51
+ rmfmail's existing config-editor/watch machinery; imail (self-contained by
52
+ policy) keeps its own cached copy.
53
+ - **Shared SQLite over SMB is ruled out** (WAL needs shared memory; SMB locking
54
+ corrupts). If a shared rich store is ever needed: a small annotation service
55
+ owning WAL SQLite behind HTTP/MCP (the rmfmail-daemon pattern), or libSQL
56
+ embedded replicas for multi-machine.
57
+
58
+ ## Fit with existing TODO items
59
+
60
+ - **Q113 / Q113-AI** (per-message metadata shape): this design answers the
61
+ cross-app half — message-attached keywords + registry. A local
62
+ `message_meta` table remains a possible rmfmail-internal cache/residue store.
63
+ - **Q103-AI / C39 / P19** (rules engine + screener): `sortAction` in the
64
+ registry is a candidate rule shape; imail experiments feed this directly.
65
+
66
+ ## Open questions for Bob
67
+
68
+ 1. Starter vocabulary — which categories actually earn one of the ~26 slots?
69
+ 2. Does the annotator mirror keywords → Gmail labels / Graph categories from
70
+ day one, or Dovecot-only first (bobma is the hot path)?
71
+ 3. Where does the annotator run — IDLE-driven daemon on new mail (before
72
+ imail's scheduled pass), or a stage inside imail's own loop?
73
+ 4. Should rmfmail render `im_*` keywords as chips in the message list (it
74
+ already syncs them in `flags_json` — display-only change)?
@@ -0,0 +1,78 @@
1
+ // Annotation registry — DRAFT starter vocabulary (see annotation-registry.md).
2
+ // One entry per IMAP keyword the companion annotator may set. MCP-tool-style
3
+ // descriptions: `description` + `whenToApply` are written for an AI annotator
4
+ // the same way a tool description is written for an agent; `sortAction` is
5
+ // read by the sorter; `label` by UIs. Keywords are IMAP atoms: ASCII, no
6
+ // spaces, always prefixed `im_`. Budget: Dovecot/maildir allows ~26 keywords
7
+ // per mailbox — keep this list SMALL and curated.
8
+ {
9
+ "version": 1,
10
+ "keywords": {
11
+ // ── Categories (mutually exclusive — at most one per message) ──
12
+ "im_bills": {
13
+ "label": "Bills & orders",
14
+ "description": "Invoices, receipts, order/shipping confirmations from a vendor the user has transacted with.",
15
+ "whenToApply": "Sender is a merchant/utility/bank AND the body references a specific transaction, amount, order number, or account.",
16
+ "exclusiveGroup": "category",
17
+ "setBy": ["annotator"],
18
+ "sortAction": { "moveTo": "Added/bills and orders" }
19
+ },
20
+ "im_newsletter": {
21
+ "label": "Newsletter",
22
+ "description": "Bulk editorial content the user subscribed to — digests, columns, mailing-list posts that are broadcast rather than personal.",
23
+ "whenToApply": "List-Unsubscribe header present AND content is editorial/broadcast, not transactional and not addressed to the user personally.",
24
+ "exclusiveGroup": "category",
25
+ "setBy": ["annotator"],
26
+ "sortAction": { "moveTo": "Added/newsletters" }
27
+ },
28
+ "im_political": {
29
+ "label": "Political",
30
+ "description": "Campaign fundraising, advocacy, and political-action mail.",
31
+ "whenToApply": "Sender is a campaign/PAC/advocacy org, or body is dominated by donation asks or political calls to action.",
32
+ "exclusiveGroup": "category",
33
+ "setBy": ["annotator"],
34
+ "sortAction": { "moveTo": "Added/political" }
35
+ },
36
+ "im_event": {
37
+ "label": "Event / invite",
38
+ "description": "Invitations and event announcements with a concrete date/time the user could attend.",
39
+ "whenToApply": "Body or attachment carries a specific event with date/time/place (calendar invite, webinar, meetup, talk).",
40
+ "exclusiveGroup": "category",
41
+ "setBy": ["annotator"],
42
+ // Rich residue (the extracted datetime) doesn't fit a keyword —
43
+ // the annotator writes the .ics file, as imail already does.
44
+ "sortAction": { "keep": true }
45
+ },
46
+ "im_personal": {
47
+ "label": "Personal",
48
+ "description": "A human wrote this specifically to the user — correspondence, not broadcast.",
49
+ "whenToApply": "Individual sender, no unsubscribe machinery, content addresses the user or an ongoing thread.",
50
+ "exclusiveGroup": "category",
51
+ "setBy": ["annotator"],
52
+ "sortAction": { "keep": true }
53
+ },
54
+
55
+ // ── Independent signals (combinable) ──
56
+ "im_spam_high": {
57
+ "label": "Likely spam",
58
+ "description": "High-confidence spam beyond what SpamAssassin already headers — bucketed score, since keywords cannot carry values.",
59
+ "whenToApply": "Model confidence ≥ 0.9 that the message is unsolicited bulk/scam. Never apply on mere newsletter-ness.",
60
+ "setBy": ["annotator"],
61
+ "sortAction": { "moveTo": "Spam" }
62
+ },
63
+ "im_action": {
64
+ "label": "Action required",
65
+ "description": "The user is being asked to do something with a real deadline or dependency (reply, pay, RSVP, sign, review).",
66
+ "whenToApply": "Explicit request directed at the user with a deadline or a blocked party waiting.",
67
+ "setBy": ["annotator"],
68
+ "sortAction": { "keep": true }
69
+ },
70
+ "im_screened": {
71
+ "label": "Screened",
72
+ "description": "Bookkeeping: the annotator has processed this message. Lets the sorter and re-runs skip already-annotated mail cheaply.",
73
+ "whenToApply": "Set on every message the annotator completes, regardless of verdict.",
74
+ "setBy": ["annotator"],
75
+ "sortAction": { "keep": true }
76
+ }
77
+ }
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/rmfmail",
3
- "version": "1.2.143",
3
+ "version": "1.2.144",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -40,7 +40,7 @@
40
40
  "@bobfrankston/mailx-store-web": "^0.1.38",
41
41
  "@bobfrankston/mailx-sync": "^0.1.27",
42
42
  "@bobfrankston/miscinfo": "^1.0.13",
43
- "@bobfrankston/msger": "^0.1.394",
43
+ "@bobfrankston/msger": "^0.1.395",
44
44
  "@bobfrankston/node-tcp-transport": "^0.1.10",
45
45
  "@bobfrankston/oauthsupport": "^1.0.34",
46
46
  "@bobfrankston/rmf-tiny": "^0.1.38",
@@ -118,7 +118,7 @@
118
118
  "@bobfrankston/mailx-store-web": "^0.1.38",
119
119
  "@bobfrankston/mailx-sync": "^0.1.27",
120
120
  "@bobfrankston/miscinfo": "^1.0.13",
121
- "@bobfrankston/msger": "^0.1.394",
121
+ "@bobfrankston/msger": "^0.1.395",
122
122
  "@bobfrankston/node-tcp-transport": "^0.1.10",
123
123
  "@bobfrankston/oauthsupport": "^1.0.34",
124
124
  "@bobfrankston/rmf-tiny": "^0.1.38",