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.
Files changed (38) hide show
  1. correspondence_kit-0.1.0/.claude/settings.local.json +18 -0
  2. correspondence_kit-0.1.0/.claude/skills/email/SKILL.md +44 -0
  3. correspondence_kit-0.1.0/.claude/skills/email/find_unanswered.py +56 -0
  4. correspondence_kit-0.1.0/.env.example +9 -0
  5. correspondence_kit-0.1.0/.gitignore +13 -0
  6. correspondence_kit-0.1.0/.make/common.mk +18 -0
  7. correspondence_kit-0.1.0/.mise.toml +9 -0
  8. correspondence_kit-0.1.0/.python-version +1 -0
  9. correspondence_kit-0.1.0/AGENTS.md +346 -0
  10. correspondence_kit-0.1.0/CLAUDE.md +1 -0
  11. correspondence_kit-0.1.0/LICENSE +190 -0
  12. correspondence_kit-0.1.0/Makefile +1 -0
  13. correspondence_kit-0.1.0/PKG-INFO +16 -0
  14. correspondence_kit-0.1.0/README.md +45 -0
  15. correspondence_kit-0.1.0/collaborators.toml +6 -0
  16. correspondence_kit-0.1.0/pyproject.toml +60 -0
  17. correspondence_kit-0.1.0/src/audit_docs.py +247 -0
  18. correspondence_kit-0.1.0/src/cloudflare/__init__.py +0 -0
  19. correspondence_kit-0.1.0/src/collab/__init__.py +46 -0
  20. correspondence_kit-0.1.0/src/collab/add.py +199 -0
  21. correspondence_kit-0.1.0/src/collab/remove.py +80 -0
  22. correspondence_kit-0.1.0/src/collab/sync.py +149 -0
  23. correspondence_kit-0.1.0/src/draft/__init__.py +0 -0
  24. correspondence_kit-0.1.0/src/draft/push.py +160 -0
  25. correspondence_kit-0.1.0/src/help.py +49 -0
  26. correspondence_kit-0.1.0/src/sync/__init__.py +0 -0
  27. correspondence_kit-0.1.0/src/sync/auth.py +49 -0
  28. correspondence_kit-0.1.0/src/sync/gmail.py +419 -0
  29. correspondence_kit-0.1.0/src/sync/types.py +27 -0
  30. correspondence_kit-0.1.0/tests/conftest.py +9 -0
  31. correspondence_kit-0.1.0/tests/test_collab_add.py +249 -0
  32. correspondence_kit-0.1.0/tests/test_collab_config.py +79 -0
  33. correspondence_kit-0.1.0/tests/test_collab_remove.py +147 -0
  34. correspondence_kit-0.1.0/tests/test_collab_sync.py +169 -0
  35. correspondence_kit-0.1.0/tests/test_draft_push.py +125 -0
  36. correspondence_kit-0.1.0/tests/test_gmail_sync.py +224 -0
  37. correspondence_kit-0.1.0/uv.lock +554 -0
  38. 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,13 @@
1
+ .env
2
+ AGENTS.local.md
3
+ CLAUDE.local.md
4
+ conversations/
5
+ drafts/
6
+ *.credentials.json
7
+ credentials.json
8
+ .venv/
9
+ __pycache__/
10
+
11
+ .sync-state.json
12
+ .idea/
13
+ tmp/
@@ -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,9 @@
1
+ [env]
2
+ _.file = [".env"]
3
+ _.path = [".bin"]
4
+
5
+ [tools]
6
+ bun = "latest"
7
+
8
+ [settings]
9
+ python.uv_venv_auto = true
@@ -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