correspondence-kit 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.
- correspondence_kit-0.1.0/.claude/settings.local.json +18 -0
- correspondence_kit-0.1.0/.claude/skills/email/SKILL.md +44 -0
- correspondence_kit-0.1.0/.claude/skills/email/find_unanswered.py +56 -0
- correspondence_kit-0.1.0/.env.example +9 -0
- correspondence_kit-0.1.0/.gitignore +13 -0
- correspondence_kit-0.1.0/.make/common.mk +18 -0
- correspondence_kit-0.1.0/.mise.toml +9 -0
- correspondence_kit-0.1.0/.python-version +1 -0
- correspondence_kit-0.1.0/AGENTS.md +346 -0
- correspondence_kit-0.1.0/CLAUDE.md +1 -0
- correspondence_kit-0.1.0/LICENSE +190 -0
- correspondence_kit-0.1.0/Makefile +1 -0
- correspondence_kit-0.1.0/PKG-INFO +16 -0
- correspondence_kit-0.1.0/README.md +45 -0
- correspondence_kit-0.1.0/collaborators.toml +6 -0
- correspondence_kit-0.1.0/pyproject.toml +60 -0
- correspondence_kit-0.1.0/src/audit_docs.py +247 -0
- correspondence_kit-0.1.0/src/cloudflare/__init__.py +0 -0
- correspondence_kit-0.1.0/src/collab/__init__.py +46 -0
- correspondence_kit-0.1.0/src/collab/add.py +199 -0
- correspondence_kit-0.1.0/src/collab/remove.py +80 -0
- correspondence_kit-0.1.0/src/collab/sync.py +149 -0
- correspondence_kit-0.1.0/src/draft/__init__.py +0 -0
- correspondence_kit-0.1.0/src/draft/push.py +160 -0
- correspondence_kit-0.1.0/src/help.py +49 -0
- correspondence_kit-0.1.0/src/sync/__init__.py +0 -0
- correspondence_kit-0.1.0/src/sync/auth.py +49 -0
- correspondence_kit-0.1.0/src/sync/gmail.py +419 -0
- correspondence_kit-0.1.0/src/sync/types.py +27 -0
- correspondence_kit-0.1.0/tests/conftest.py +9 -0
- correspondence_kit-0.1.0/tests/test_collab_add.py +249 -0
- correspondence_kit-0.1.0/tests/test_collab_config.py +79 -0
- correspondence_kit-0.1.0/tests/test_collab_remove.py +147 -0
- correspondence_kit-0.1.0/tests/test_collab_sync.py +169 -0
- correspondence_kit-0.1.0/tests/test_draft_push.py +125 -0
- correspondence_kit-0.1.0/tests/test_gmail_sync.py +224 -0
- correspondence_kit-0.1.0/uv.lock +554 -0
- correspondence_kit-0.1.0/voice.md +19 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"WebFetch(domain:harper.blog)",
|
|
5
|
+
"WebFetch(domain:jlongster.com)",
|
|
6
|
+
"Bash(uv sync:*)",
|
|
7
|
+
"Bash(uv run:*)",
|
|
8
|
+
"Bash(python3:*)",
|
|
9
|
+
"Bash(uv add:*)",
|
|
10
|
+
"Bash(git -C /home/brian/work/btakita/correspondence-kit log --oneline -10)",
|
|
11
|
+
"Bash(git -C /home/brian/work/btakita/correspondence-kit status)",
|
|
12
|
+
"Bash(git -C /home/brian/work/btakita/correspondence-kit check-ignore .env CLAUDE.local.md conversations/ drafts/)",
|
|
13
|
+
"Bash(git -C /home/brian/work/btakita/correspondence-kit log --oneline --all)",
|
|
14
|
+
"WebSearch",
|
|
15
|
+
"WebFetch(domain:pypi.org)"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Email Skill
|
|
2
|
+
|
|
3
|
+
Manage and draft correspondence using locally synced Gmail threads.
|
|
4
|
+
|
|
5
|
+
## Core Principles
|
|
6
|
+
|
|
7
|
+
- **Draft only** — never send email directly; always save as a Gmail draft for human review
|
|
8
|
+
- **Match voice** — follow the Writing Voice guidelines in CLAUDE.md exactly
|
|
9
|
+
- **Use context** — always read the relevant thread in `conversations/` before drafting a reply
|
|
10
|
+
- **Be concise** — prefer shorter responses; ask before writing anything long
|
|
11
|
+
|
|
12
|
+
## Available Tools
|
|
13
|
+
|
|
14
|
+
- `conversations/` — synced email threads as Markdown, organized by label
|
|
15
|
+
- `drafts/` — outgoing email drafts being worked on
|
|
16
|
+
- `uv run .claude/skills/email/find_unanswered.py` — list threads awaiting a reply
|
|
17
|
+
- `uv run src/sync/gmail.py` — re-sync threads from Gmail
|
|
18
|
+
|
|
19
|
+
## Workflows
|
|
20
|
+
|
|
21
|
+
### Review inbox
|
|
22
|
+
1. Run `find_unanswered.py` to identify threads needing a reply
|
|
23
|
+
2. Read each thread and assess priority
|
|
24
|
+
3. Present a prioritized list with a one-line summary per thread
|
|
25
|
+
4. Wait for instruction before drafting anything
|
|
26
|
+
|
|
27
|
+
### Draft a reply
|
|
28
|
+
1. Read the full thread from `conversations/`
|
|
29
|
+
2. Identify the key ask or context requiring a response
|
|
30
|
+
3. Draft a reply in `drafts/[YYYY-MM-DD]-[slug].md` matching the voice guidelines
|
|
31
|
+
4. Present the draft and ask for feedback before finalizing
|
|
32
|
+
5. Iterate until approved — then offer to save as a Gmail draft
|
|
33
|
+
|
|
34
|
+
### Draft a new email
|
|
35
|
+
1. Ask for: recipient, topic, any relevant context or linked threads
|
|
36
|
+
2. Draft in `drafts/[YYYY-MM-DD]-[slug].md`
|
|
37
|
+
3. Iterate until approved
|
|
38
|
+
|
|
39
|
+
## Success Criteria
|
|
40
|
+
|
|
41
|
+
- Drafts sound like the user wrote them, not like an AI
|
|
42
|
+
- No email is ever sent without explicit approval
|
|
43
|
+
- Threads are read in full before drafting — no assumptions from subject alone
|
|
44
|
+
- Priority assessment reflects the user's relationships and context, not just recency
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
List synced threads where the last message is not from you —
|
|
3
|
+
i.e. threads awaiting your reply.
|
|
4
|
+
|
|
5
|
+
Usage: uv run .claude/skills/email/find_unanswered.py
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
|
|
14
|
+
load_dotenv()
|
|
15
|
+
|
|
16
|
+
USER_EMAIL = os.getenv("GMAIL_USER_EMAIL", "").lower()
|
|
17
|
+
if not USER_EMAIL:
|
|
18
|
+
print("GMAIL_USER_EMAIL not set in .env", file=sys.stderr)
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
CONVERSATIONS = Path("conversations")
|
|
22
|
+
if not CONVERSATIONS.exists():
|
|
23
|
+
print("No conversations/ directory found. Run sync first.", file=sys.stderr)
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
|
|
26
|
+
# Match "## Sender Name <email@example.com> — Date" or "## Name — Date"
|
|
27
|
+
SENDER_RE = re.compile(r"^## (.+?) —", re.MULTILINE)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def last_sender(text: str) -> str:
|
|
31
|
+
matches = SENDER_RE.findall(text)
|
|
32
|
+
return matches[-1].strip() if matches else ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
unanswered: list[tuple[str, str, str]] = [] # (label, filename, last_sender)
|
|
36
|
+
|
|
37
|
+
thread_files = sorted(
|
|
38
|
+
CONVERSATIONS.rglob("*.md"), key=lambda p: p.name, reverse=True
|
|
39
|
+
)
|
|
40
|
+
for thread_file in thread_files:
|
|
41
|
+
text = thread_file.read_text(encoding="utf-8")
|
|
42
|
+
sender = last_sender(text)
|
|
43
|
+
# Consider it unanswered if the last sender doesn't contain the user's email or name
|
|
44
|
+
if USER_EMAIL not in sender.lower():
|
|
45
|
+
label = thread_file.parent.name
|
|
46
|
+
unanswered.append((label, thread_file.name, sender))
|
|
47
|
+
|
|
48
|
+
if not unanswered:
|
|
49
|
+
print("No unanswered threads found.")
|
|
50
|
+
sys.exit(0)
|
|
51
|
+
|
|
52
|
+
print(f"Unanswered threads ({len(unanswered)}):\n")
|
|
53
|
+
for label, filename, sender in unanswered:
|
|
54
|
+
print(f" [{label}] {filename}")
|
|
55
|
+
print(f" Last from: {sender}")
|
|
56
|
+
print()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
GMAIL_USER_EMAIL=
|
|
2
|
+
GMAIL_APP_PASSWORD= # myaccount.google.com/apppasswords
|
|
3
|
+
GMAIL_SYNC_LABELS=correspondence
|
|
4
|
+
GMAIL_SYNC_DAYS=3650
|
|
5
|
+
|
|
6
|
+
# Cloudflare (optional — for routing intelligence)
|
|
7
|
+
CLOUDFLARE_ACCOUNT_ID=
|
|
8
|
+
CLOUDFLARE_API_TOKEN=
|
|
9
|
+
CLOUDFLARE_D1_DATABASE_ID=
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
init: PY_VERSION = $(shell [ -f .python-version ] && \
|
|
2
|
+
cat .python-version || \
|
|
3
|
+
uv run python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" \
|
|
4
|
+
)
|
|
5
|
+
init:
|
|
6
|
+
@echo "Using Python version: $(PY_VERSION)"
|
|
7
|
+
|
|
8
|
+
@if command -v mise >/dev/null 2>&1; then \
|
|
9
|
+
mise install; \
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
uv venv .venv --python "$(PY_VERSION)" --no-project --clear --seed $(VENV_ARGS)
|
|
13
|
+
|
|
14
|
+
@if [ -n "$(ALL)" ]; then \
|
|
15
|
+
uv sync --python "$(PY_VERSION)" --all-groups --all-extras $(SYNC_ARGS); \
|
|
16
|
+
else \
|
|
17
|
+
uv sync --python "$(PY_VERSION)" $(SYNC_ARGS); \
|
|
18
|
+
fi
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# Correspondence Kit
|
|
2
|
+
|
|
3
|
+
A personal workspace for drafting emails and syncing conversation threads from Gmail (and eventually Protonmail).
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
- Sync email threads from Gmail by label into local Markdown files
|
|
8
|
+
- Draft and refine outgoing emails with Claude's assistance
|
|
9
|
+
- Maintain a readable, version-controlled record of correspondence
|
|
10
|
+
- Push distilled intelligence (tags, routing rules, contact metadata) to Cloudflare for email routing
|
|
11
|
+
|
|
12
|
+
## Tech Stack
|
|
13
|
+
|
|
14
|
+
- **Runtime**: Python 3.12+ via `uv`
|
|
15
|
+
- **Linter/formatter**: `ruff`
|
|
16
|
+
- **Type checker**: `ty`
|
|
17
|
+
- **Types/serialization**: `msgspec` (Struct instead of dataclasses)
|
|
18
|
+
- **Storage**: Markdown files (one file per conversation thread)
|
|
19
|
+
- **Email sources**: Gmail (via Gmail API), Protonmail (planned)
|
|
20
|
+
- **Cloudflare** (routing layer): TypeScript Workers reading from D1/KV populated by Python
|
|
21
|
+
|
|
22
|
+
## Project Structure
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
correspondence-kit/
|
|
26
|
+
AGENTS.md # Project instructions (CLAUDE.md symlinks here)
|
|
27
|
+
pyproject.toml
|
|
28
|
+
voice.md # Writing voice guidelines (committed)
|
|
29
|
+
collaborators.toml # Collaborator config (committed)
|
|
30
|
+
.env # OAuth credentials and config (gitignored)
|
|
31
|
+
.gitignore
|
|
32
|
+
.claude/
|
|
33
|
+
skills/
|
|
34
|
+
email/
|
|
35
|
+
SKILL.md # Email drafting & management skill
|
|
36
|
+
find_unanswered.py # Find threads needing a reply
|
|
37
|
+
src/
|
|
38
|
+
sync/
|
|
39
|
+
__init__.py
|
|
40
|
+
gmail.py # Gmail API sync logic
|
|
41
|
+
types.py # msgspec Structs (Thread, Message, etc.)
|
|
42
|
+
auth.py # One-time OAuth flow
|
|
43
|
+
draft/
|
|
44
|
+
__init__.py
|
|
45
|
+
push.py # Push draft to Gmail (draft or send)
|
|
46
|
+
collab/
|
|
47
|
+
__init__.py # Collaborator config parser (collaborators.toml)
|
|
48
|
+
add.py # collab-add command
|
|
49
|
+
sync.py # collab-sync / collab-status commands
|
|
50
|
+
remove.py # collab-remove command
|
|
51
|
+
cloudflare/
|
|
52
|
+
__init__.py
|
|
53
|
+
push.py # Push intelligence to Cloudflare D1/KV
|
|
54
|
+
conversations/ # Synced threads (gitignored — private)
|
|
55
|
+
[label]/
|
|
56
|
+
[YYYY-MM-DD]-[subject].md
|
|
57
|
+
drafts/ # Outgoing email drafts
|
|
58
|
+
[YYYY-MM-DD]-[subject].md
|
|
59
|
+
shared/ # Collaborator submodules (tracked by git)
|
|
60
|
+
[name]/ # submodule → btakita/correspondence-shared-[name]
|
|
61
|
+
conversations/[label]/*.md
|
|
62
|
+
drafts/*.md
|
|
63
|
+
AGENTS.md
|
|
64
|
+
voice.md
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Writing Voice
|
|
68
|
+
|
|
69
|
+
See `voice.md` (committed) for tone, style, and formatting guidelines.
|
|
70
|
+
|
|
71
|
+
## Safety Rules
|
|
72
|
+
|
|
73
|
+
- **Never send email directly.** Always save as a Gmail draft for review first.
|
|
74
|
+
- **Never guess at intent.** If the right response is unclear, ask rather than assume.
|
|
75
|
+
- **Never share conversation content** outside this local environment (no third-party APIs) unless explicitly instructed.
|
|
76
|
+
|
|
77
|
+
## Environment Setup
|
|
78
|
+
|
|
79
|
+
Copy `.env.example` to `.env` and fill in credentials:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
cp .env.example .env
|
|
83
|
+
uv sync
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Required variables in `.env`:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
GMAIL_CLIENT_ID=
|
|
90
|
+
GMAIL_CLIENT_SECRET=
|
|
91
|
+
GMAIL_REDIRECT_URI=http://localhost:3000/oauth/callback
|
|
92
|
+
GMAIL_REFRESH_TOKEN=
|
|
93
|
+
GMAIL_USER_EMAIL= # Your Gmail address (used to detect unanswered threads)
|
|
94
|
+
GMAIL_SYNC_LABELS=correspondence # comma-separated Gmail labels to sync (your private labels)
|
|
95
|
+
|
|
96
|
+
# Cloudflare (optional — for routing intelligence)
|
|
97
|
+
CLOUDFLARE_ACCOUNT_ID=
|
|
98
|
+
CLOUDFLARE_API_TOKEN=
|
|
99
|
+
CLOUDFLARE_D1_DATABASE_ID=
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Gmail OAuth Setup
|
|
103
|
+
|
|
104
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
105
|
+
2. Create a project → Enable the **Gmail API**
|
|
106
|
+
3. Create OAuth 2.0 credentials (Desktop app type)
|
|
107
|
+
4. Download the credentials JSON and extract `client_id` and `client_secret` into `.env`
|
|
108
|
+
5. Run the auth flow once to obtain a refresh token:
|
|
109
|
+
```sh
|
|
110
|
+
uv run sync-auth
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Commands
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
uv sync # Install dependencies
|
|
117
|
+
uv run sync-auth # One-time Gmail OAuth setup
|
|
118
|
+
uv run sync-gmail # Incremental sync (only new messages)
|
|
119
|
+
uv run sync-gmail --full # Full re-sync (ignores saved state)
|
|
120
|
+
uv run .claude/skills/email/find_unanswered.py # List threads needing a reply
|
|
121
|
+
uv run push-draft drafts/FILE.md # Save draft to Gmail
|
|
122
|
+
uv run push-draft drafts/FILE.md --send # Send email
|
|
123
|
+
uv run pytest # Run tests
|
|
124
|
+
uv run ruff check . # Lint
|
|
125
|
+
uv run ruff format . # Format
|
|
126
|
+
uv run ty check # Type check
|
|
127
|
+
|
|
128
|
+
# Collaborator management
|
|
129
|
+
uv run collab-add NAME --label LABEL [--github-user USER | --pat] [--public]
|
|
130
|
+
uv run collab-sync [NAME] # Push/pull shared submodules
|
|
131
|
+
uv run collab-status # Quick check for pending changes
|
|
132
|
+
uv run collab-remove NAME [--delete-repo]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Workflows
|
|
136
|
+
|
|
137
|
+
### Daily email review
|
|
138
|
+
|
|
139
|
+
1. Run `uv run src/sync/gmail.py` to pull latest threads
|
|
140
|
+
2. Ask Claude: *"Review conversations/ and identify threads that need a response, ordered by priority"*
|
|
141
|
+
3. For each thread, ask Claude to draft a reply matching the voice guidelines above
|
|
142
|
+
4. Review and edit the draft in `drafts/`
|
|
143
|
+
5. When satisfied, ask Claude to save it as a Gmail draft (never send directly)
|
|
144
|
+
|
|
145
|
+
### Finding unanswered threads
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
uv run .claude/skills/email/find_unanswered.py
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Lists all synced threads where the last message is not from you — i.e. threads awaiting your reply.
|
|
152
|
+
|
|
153
|
+
### Drafting a new email
|
|
154
|
+
|
|
155
|
+
Ask Claude: *"Draft an email to [person] about [topic]"* — point it at any relevant thread in `conversations/` for context.
|
|
156
|
+
|
|
157
|
+
## Gmail Sync Behavior
|
|
158
|
+
|
|
159
|
+
- **Incremental by default**: Tracks IMAP UIDs in `.sync-state.json` (gitignored). Only new messages since
|
|
160
|
+
last sync are fetched. Use `--full` to ignore saved state and re-fetch everything.
|
|
161
|
+
- **Streaming writes**: Each message is merged into its thread file immediately after fetch — no batching.
|
|
162
|
+
If sync crashes mid-run, state is not saved; next run re-fetches from last good state.
|
|
163
|
+
- **UIDVALIDITY**: If the IMAP server resets UIDVALIDITY for a folder, that label automatically does a full resync.
|
|
164
|
+
- **Label routing**: Labels from `GMAIL_SYNC_LABELS` go to `conversations/{label}/`. Labels listed in
|
|
165
|
+
`collaborators.toml` are automatically included in the sync and routed to `shared/{name}/conversations/{label}/`.
|
|
166
|
+
A thread only needs the shared label (e.g. `for-alex`) -- no need to also label it `correspondence`.
|
|
167
|
+
- Threads are written to `[YYYY-MM-DD]-[slug].md`
|
|
168
|
+
- Date is derived from the most recent message in the thread
|
|
169
|
+
- Slug is derived from the subject line
|
|
170
|
+
- Existing thread files are matched by `**Thread ID**` metadata, not filename
|
|
171
|
+
- New messages are deduplicated by `(sender, date)` tuple when merging into existing files
|
|
172
|
+
- Attachments are noted inline but not downloaded
|
|
173
|
+
|
|
174
|
+
## Cloudflare Architecture
|
|
175
|
+
|
|
176
|
+
Python handles the heavy lifting locally. Distilled intelligence is pushed to Cloudflare storage for use by a lightweight TypeScript Worker that handles email routing.
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
Gmail/Protonmail
|
|
180
|
+
↓
|
|
181
|
+
Python (local, uv)
|
|
182
|
+
- sync threads → markdown
|
|
183
|
+
- extract intelligence (tags, contact metadata, routing rules)
|
|
184
|
+
- push to Cloudflare
|
|
185
|
+
↓
|
|
186
|
+
Cloudflare D1 / KV
|
|
187
|
+
- contact importance scores
|
|
188
|
+
- thread tags / inferred topics
|
|
189
|
+
- routing rules
|
|
190
|
+
↓
|
|
191
|
+
Cloudflare Worker (TypeScript)
|
|
192
|
+
- email routing decisions using intelligence from Python
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Full conversation threads stay local. Cloudflare only receives the minimal distilled signal needed for routing.
|
|
196
|
+
|
|
197
|
+
## Conversation Markdown Format
|
|
198
|
+
|
|
199
|
+
Each synced thread is written in this format:
|
|
200
|
+
|
|
201
|
+
```markdown
|
|
202
|
+
# [Subject]
|
|
203
|
+
|
|
204
|
+
**Label**: [label]
|
|
205
|
+
**Thread ID**: [Gmail thread ID]
|
|
206
|
+
**Last updated**: [ISO date]
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## [Sender Name] — [Date]
|
|
211
|
+
|
|
212
|
+
[Body text]
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## [Reply sender] — [Date]
|
|
217
|
+
|
|
218
|
+
[Body text]
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Draft Format
|
|
222
|
+
|
|
223
|
+
Drafts live in `drafts/` (private) or `shared/{name}/drafts/` (collaborator). Filename convention: `[YYYY-MM-DD]-[slug].md`.
|
|
224
|
+
|
|
225
|
+
```markdown
|
|
226
|
+
# [Subject]
|
|
227
|
+
|
|
228
|
+
**To**: [recipient]
|
|
229
|
+
**CC**: (optional)
|
|
230
|
+
**Status**: draft
|
|
231
|
+
**Author**: brian
|
|
232
|
+
**In-Reply-To**: (optional — message ID)
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
[Draft body]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Status values: `draft` -> `review` -> `approved` -> `sent`
|
|
240
|
+
|
|
241
|
+
When asking Claude to help draft or refine an email:
|
|
242
|
+
- Point it at the relevant thread in `conversations/` for context
|
|
243
|
+
- Specify tone if it differs from the voice guidelines (formal, concise, etc.)
|
|
244
|
+
- Indicate any constraints (length, what to avoid, etc.)
|
|
245
|
+
|
|
246
|
+
## Collaborators
|
|
247
|
+
|
|
248
|
+
Share specific threads with collaborators via per-collaborator GitHub repos linked as submodules.
|
|
249
|
+
Collaborators can be people or AI agents -- anything that can read markdown and push to a git repo.
|
|
250
|
+
|
|
251
|
+
### Config: `collaborators.toml`
|
|
252
|
+
|
|
253
|
+
```toml
|
|
254
|
+
[alex]
|
|
255
|
+
labels = ["for-alex"]
|
|
256
|
+
repo = "btakita/correspondence-shared-alex"
|
|
257
|
+
github_user = "alex-github-username"
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### How it works
|
|
261
|
+
|
|
262
|
+
1. `collab-add` creates a private GitHub repo (or `--public`), initializes it with AGENTS.md + voice.md,
|
|
263
|
+
and adds it as a submodule under `shared/{name}/`
|
|
264
|
+
2. `sync-gmail` routes shared labels to `shared/{name}/conversations/{label}/`
|
|
265
|
+
3. `collab-sync` pushes synced conversations to the shared repo and pulls collaborator drafts
|
|
266
|
+
4. Collaborators create drafts in `shared/{name}/drafts/` with Status/Author fields
|
|
267
|
+
5. Brian reviews, approves, and sends via `push-draft`
|
|
268
|
+
|
|
269
|
+
### AI agents as collaborators
|
|
270
|
+
|
|
271
|
+
An AI agent (Codex, Claude Code, a custom agent) can be a collaborator. It reads conversations,
|
|
272
|
+
drafts replies following voice.md, and pushes to the shared repo like any other collaborator.
|
|
273
|
+
Brian still reviews and sends. Use `--pat` for token-based access when the collaborator isn't a
|
|
274
|
+
GitHub user (e.g. a CI-driven agent).
|
|
275
|
+
|
|
276
|
+
### Security model
|
|
277
|
+
|
|
278
|
+
- `.env` is gitignored -- only Brian has Gmail credentials
|
|
279
|
+
- Each shared repo is separate with per-user access control
|
|
280
|
+
- Shared repos contain ONLY threads Brian explicitly labels for that person
|
|
281
|
+
- Collaborators cannot see each other's shared repos
|
|
282
|
+
|
|
283
|
+
## MCP Alternative
|
|
284
|
+
|
|
285
|
+
Instead of pre-syncing to markdown files, Claude can access Gmail live via an MCP server during a session. Options:
|
|
286
|
+
|
|
287
|
+
- **Pipedream** — hosted MCP with Gmail, Calendar, Contacts (note: data passes through Pipedream)
|
|
288
|
+
- **Local Python MCP server** — run a Gmail MCP server locally for fully private live access (future)
|
|
289
|
+
|
|
290
|
+
Current approach (file sync) is preferred for privacy and offline use. MCP is worth revisiting for real-time workflows.
|
|
291
|
+
|
|
292
|
+
## Package-Level Instruction Files
|
|
293
|
+
|
|
294
|
+
Each subpackage (`src/sync/`, `src/draft/`, `src/cloudflare/`) can contain its own `AGENTS.md` with package-specific
|
|
295
|
+
conventions and context. These files are committed to the repo and auto-loaded when an agent works in that directory.
|
|
296
|
+
They also surface when searching dependency code across packages.
|
|
297
|
+
|
|
298
|
+
Use package-level files for deep-dives on that package's types, patterns, and gotchas. Keep the root `AGENTS.md`
|
|
299
|
+
focused on cross-cutting project concerns.
|
|
300
|
+
|
|
301
|
+
**Dual-name convention:** `AGENTS.md` is the canonical committed file, readable by Codex and other agents. `CLAUDE.md`
|
|
302
|
+
is a symlink to `AGENTS.md`, readable natively by Claude Code. For personal overrides, use `CLAUDE.local.md` (Claude
|
|
303
|
+
Code) or `AGENTS.local.md` (Codex) — both are gitignored.
|
|
304
|
+
|
|
305
|
+
**Actionable over informational.** Instruction files should contain the minimum needed to generate correct code: type
|
|
306
|
+
names, import paths, patterns, conventions, constraints. Reference material like module tables, route lists, and
|
|
307
|
+
architecture overviews belongs in `README.md`.
|
|
308
|
+
|
|
309
|
+
**Update with the code.** When a change affects patterns, conventions, type names, import paths, or module boundaries
|
|
310
|
+
documented in `AGENTS.md` or `README.md`, update those files as part of the same change.
|
|
311
|
+
|
|
312
|
+
**Stay concise.** All instruction files loaded in a session share the context window. Combined root + package files
|
|
313
|
+
should stay well under 1000 lines to avoid crowding out working context.
|
|
314
|
+
|
|
315
|
+
## Conventions
|
|
316
|
+
|
|
317
|
+
- Use `uv run` for script execution, never bare `python`
|
|
318
|
+
- Use `msgspec.Struct` for all data types — not dataclasses or TypedDict
|
|
319
|
+
- Use `ruff` for linting and formatting
|
|
320
|
+
- Use `ty` for type checking
|
|
321
|
+
- Keep sync, draft, and cloudflare logic in separate subpackages
|
|
322
|
+
- Do not commit `.env`, `CLAUDE.local.md` / `AGENTS.local.md`, or `conversations/` (private data)
|
|
323
|
+
- Scripts must be runnable directly: `uv run src/sync/gmail.py`
|
|
324
|
+
|
|
325
|
+
## Future Work
|
|
326
|
+
|
|
327
|
+
- **Project setup script**: Interactive `collab-init` or `setup` command that configures .env defaults
|
|
328
|
+
- **Protonmail sync**: Protonmail Bridge (IMAP) or Protonmail API
|
|
329
|
+
- **Cloudflare routing**: TypeScript Worker consuming D1/KV data pushed from Python
|
|
330
|
+
- **Local Gmail MCP server**: Live Gmail access during Claude sessions without Pipedream
|
|
331
|
+
- **Send integration**: Push approved drafts back to Gmail as drafts or send directly
|
|
332
|
+
- **Multi-user**: Per-user OAuth credential flow documented here when shared with another developer
|
|
333
|
+
|
|
334
|
+
## .gitignore
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
.env
|
|
338
|
+
CLAUDE.local.md
|
|
339
|
+
AGENTS.local.md
|
|
340
|
+
conversations/
|
|
341
|
+
drafts/
|
|
342
|
+
*.credentials.json
|
|
343
|
+
.sync-state.json
|
|
344
|
+
.venv/
|
|
345
|
+
__pycache__/
|
|
346
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AGENTS.md
|