@forwardimpact/basecamp 2.0.0 → 2.3.0

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 (41) hide show
  1. package/config/scheduler.json +5 -0
  2. package/package.json +1 -1
  3. package/src/basecamp.js +288 -57
  4. package/template/.claude/agents/chief-of-staff.md +6 -2
  5. package/template/.claude/agents/concierge.md +2 -3
  6. package/template/.claude/agents/librarian.md +4 -6
  7. package/template/.claude/agents/recruiter.md +269 -0
  8. package/template/.claude/settings.json +0 -4
  9. package/template/.claude/skills/analyze-cv/SKILL.md +269 -0
  10. package/template/.claude/skills/create-presentations/SKILL.md +2 -2
  11. package/template/.claude/skills/create-presentations/references/slide.css +1 -1
  12. package/template/.claude/skills/create-presentations/scripts/convert-to-pdf.mjs +47 -0
  13. package/template/.claude/skills/draft-emails/SKILL.md +85 -123
  14. package/template/.claude/skills/draft-emails/scripts/scan-emails.mjs +66 -0
  15. package/template/.claude/skills/draft-emails/scripts/send-email.mjs +118 -0
  16. package/template/.claude/skills/extract-entities/SKILL.md +2 -2
  17. package/template/.claude/skills/extract-entities/scripts/state.mjs +130 -0
  18. package/template/.claude/skills/manage-tasks/SKILL.md +242 -0
  19. package/template/.claude/skills/organize-files/SKILL.md +3 -3
  20. package/template/.claude/skills/organize-files/scripts/organize-by-type.mjs +105 -0
  21. package/template/.claude/skills/organize-files/scripts/summarize.mjs +84 -0
  22. package/template/.claude/skills/process-hyprnote/SKILL.md +2 -2
  23. package/template/.claude/skills/right-to-be-forgotten/SKILL.md +333 -0
  24. package/template/.claude/skills/send-chat/SKILL.md +170 -0
  25. package/template/.claude/skills/sync-apple-calendar/SKILL.md +5 -5
  26. package/template/.claude/skills/sync-apple-calendar/scripts/sync.mjs +325 -0
  27. package/template/.claude/skills/sync-apple-mail/SKILL.md +6 -6
  28. package/template/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +374 -0
  29. package/template/.claude/skills/sync-apple-mail/scripts/sync.mjs +629 -0
  30. package/template/.claude/skills/track-candidates/SKILL.md +376 -0
  31. package/template/.claude/skills/upstream-skill/SKILL.md +207 -0
  32. package/template/.claude/skills/weekly-update/SKILL.md +250 -0
  33. package/template/CLAUDE.md +68 -40
  34. package/template/.claude/skills/create-presentations/scripts/convert-to-pdf.js +0 -32
  35. package/template/.claude/skills/draft-emails/scripts/scan-emails.sh +0 -34
  36. package/template/.claude/skills/extract-entities/scripts/state.py +0 -100
  37. package/template/.claude/skills/organize-files/scripts/organize-by-type.sh +0 -42
  38. package/template/.claude/skills/organize-files/scripts/summarize.sh +0 -21
  39. package/template/.claude/skills/sync-apple-calendar/scripts/sync.py +0 -242
  40. package/template/.claude/skills/sync-apple-mail/scripts/parse-emlx.py +0 -104
  41. package/template/.claude/skills/sync-apple-mail/scripts/sync.py +0 -455
@@ -0,0 +1,333 @@
1
+ ---
2
+ name: right-to-be-forgotten
3
+ description: >
4
+ Process GDPR Article 17 data erasure requests. Finds and removes all personal
5
+ data related to a named individual from the knowledge base, cached data, and
6
+ agent state files. Use when the user receives a right-to-be-forgotten request,
7
+ asks to delete all data about a person, or needs to comply with a data
8
+ erasure obligation.
9
+ compatibility: Requires macOS filesystem access
10
+ ---
11
+
12
+ # Right to Be Forgotten
13
+
14
+ Process data erasure requests under GDPR Article 17 (Right to Erasure). Given a
15
+ person's name, systematically find and remove all personal data from the
16
+ knowledge base, cached synced data, and agent state files.
17
+
18
+ This skill produces an **erasure report** documenting what was found, what was
19
+ deleted, and what requires manual action — providing an audit trail for
20
+ compliance.
21
+
22
+ ## Trigger
23
+
24
+ Run this skill:
25
+
26
+ - When the user receives a formal GDPR erasure request
27
+ - When the user asks to delete all data about a specific person
28
+ - When a candidate withdraws from a recruitment process and requests data
29
+ deletion
30
+ - When the user asks to "forget" someone
31
+
32
+ ## Prerequisites
33
+
34
+ - The person's full name (and any known aliases or email addresses)
35
+ - User confirmation before deletion proceeds
36
+
37
+ ## Inputs
38
+
39
+ - **Name**: Full name of the data subject (required)
40
+ - **Aliases**: Alternative names, maiden names, nicknames (optional)
41
+ - **Email addresses**: Known email addresses (optional, improves search coverage)
42
+ - **Scope**: `all` (default) or `recruitment-only` (limits to candidate data)
43
+
44
+ ## Outputs
45
+
46
+ - `knowledge/Erasure/{Name}--{YYYY-MM-DD}.md` — erasure report (audit trail)
47
+ - Deleted files and redacted references across the knowledge base
48
+
49
+ ---
50
+
51
+ ## Step 0: Confirm Intent
52
+
53
+ Before proceeding, clearly state to the user:
54
+
55
+ > **Data erasure request for: {Name}**
56
+ >
57
+ > This will permanently delete all personal data related to {Name} from:
58
+ > - Knowledge base notes (People, Candidates, Organizations mentions)
59
+ > - Cached email threads and attachments
60
+ > - Agent state and triage files
61
+ >
62
+ > This action cannot be undone. Proceed?
63
+
64
+ **Wait for explicit confirmation before continuing.**
65
+
66
+ ## Step 1: Discovery — Find All References
67
+
68
+ Search systematically across every data location. Record every match.
69
+
70
+ ### 1a. Knowledge Base — Direct Notes
71
+
72
+ ```bash
73
+ # Candidate directory (recruitment data)
74
+ ls -d "knowledge/Candidates/{Name}/" 2>/dev/null
75
+
76
+ # People note
77
+ ls "knowledge/People/{Name}.md" 2>/dev/null
78
+
79
+ # Try common name variations
80
+ ls "knowledge/People/{First} {Last}.md" 2>/dev/null
81
+ ls "knowledge/People/{Last}, {First}.md" 2>/dev/null
82
+ ```
83
+
84
+ ### 1b. Knowledge Base — Backlinks and Mentions
85
+
86
+ ```bash
87
+ # Search for all mentions across the entire knowledge graph
88
+ rg -l "{Name}" knowledge/
89
+ rg -l "{First name} {Last name}" knowledge/
90
+
91
+ # Search for Obsidian-style links
92
+ rg -l "\[\[.*{Name}.*\]\]" knowledge/
93
+
94
+ # Search by email address if known
95
+ rg -l "{email}" knowledge/
96
+ ```
97
+
98
+ ### 1c. Cached Data — Email Threads
99
+
100
+ ```bash
101
+ # Search synced email threads for mentions
102
+ rg -l "{Name}" ~/.cache/fit/basecamp/apple_mail/ 2>/dev/null
103
+ rg -l "{email}" ~/.cache/fit/basecamp/apple_mail/ 2>/dev/null
104
+
105
+ # Check for attachment directories containing their files
106
+ find ~/.cache/fit/basecamp/apple_mail/attachments/ -iname "*{Name}*" 2>/dev/null
107
+ ```
108
+
109
+ ### 1d. Cached Data — Calendar Events
110
+
111
+ ```bash
112
+ # Search calendar events
113
+ rg -l "{Name}" ~/.cache/fit/basecamp/apple_calendar/ 2>/dev/null
114
+ rg -l "{email}" ~/.cache/fit/basecamp/apple_calendar/ 2>/dev/null
115
+ ```
116
+
117
+ ### 1e. Agent State Files
118
+
119
+ ```bash
120
+ # Search triage files for mentions
121
+ rg -l "{Name}" ~/.cache/fit/basecamp/state/ 2>/dev/null
122
+ ```
123
+
124
+ ### 1f. Drafts
125
+
126
+ ```bash
127
+ # Search email drafts
128
+ rg -l "{Name}" drafts/ 2>/dev/null
129
+ ```
130
+
131
+ Compile a complete inventory of every file and reference found.
132
+
133
+ ## Step 2: Classify References
134
+
135
+ For each discovered reference, classify the required action:
136
+
137
+ | Reference Type | Action | Example |
138
+ | --- | --- | --- |
139
+ | **Dedicated note** (sole subject) | Delete entire file | `knowledge/People/{Name}.md` |
140
+ | **Dedicated directory** | Delete entire directory | `knowledge/Candidates/{Name}/` |
141
+ | **Mention in another note** | Redact: remove lines referencing the person | Backlink in `knowledge/Organizations/Agency.md` |
142
+ | **Email thread** (sole subject) | Delete file | `~/.cache/fit/basecamp/apple_mail/thread.md` |
143
+ | **Email thread** (multiple people) | Redact: remove paragraphs about the person | Thread discussing multiple candidates |
144
+ | **Attachment** (their CV, etc.) | Delete file | `attachments/{thread}/CV.pdf` |
145
+ | **Triage/state file** | Redact: remove lines mentioning them | `recruiter_triage.md` |
146
+ | **Insights file** | Redact: remove bullets mentioning them | `knowledge/Candidates/Insights.md` |
147
+
148
+ ## Step 3: Execute Deletions
149
+
150
+ Process in order from most specific to most general:
151
+
152
+ ### 3a. Delete Dedicated Files and Directories
153
+
154
+ ```bash
155
+ # Remove candidate directory (CV, brief, assessment — everything)
156
+ rm -rf "knowledge/Candidates/{Name}/"
157
+
158
+ # Remove people note
159
+ rm -f "knowledge/People/{Name}.md"
160
+
161
+ # Remove any attachments
162
+ find ~/.cache/fit/basecamp/apple_mail/attachments/ -iname "*{Name}*" -delete
163
+ ```
164
+
165
+ ### 3b. Redact Mentions in Other Notes
166
+
167
+ For each file that **mentions** the person but isn't dedicated to them:
168
+
169
+ 1. Read the file
170
+ 2. Remove lines, bullets, or sections that reference the person
171
+ 3. Remove broken `[[backlinks]]` to deleted notes
172
+ 4. Write the updated file
173
+
174
+ **Redaction rules:**
175
+
176
+ - Remove entire bullet points that mention the person by name
177
+ - Remove table rows containing the person's name
178
+ - Remove `## Connected to` entries linking to their deleted note
179
+ - If a section becomes empty after redaction, remove the section header too
180
+ - Do NOT remove surrounding context that doesn't identify the person
181
+
182
+ ### 3c. Handle Email Threads
183
+
184
+ For threads where the person is the **sole subject** (e.g., a recruitment email
185
+ about only them):
186
+
187
+ ```bash
188
+ rm -f "~/.cache/fit/basecamp/apple_mail/{thread}.md"
189
+ ```
190
+
191
+ For threads with **multiple people**, redact only the paragraphs about this
192
+ person — leave the rest intact.
193
+
194
+ ### 3d. Clean Agent State
195
+
196
+ Remove mentions from triage files:
197
+
198
+ ```bash
199
+ # Regenerate triage files on next agent wake — just remove current mentions
200
+ for f in ~/.cache/fit/basecamp/state/*_triage.md; do
201
+ if rg -q "{Name}" "$f" 2>/dev/null; then
202
+ # Read, remove lines mentioning the person, write back
203
+ rg -v "{Name}" "$f" > "$f.tmp" && mv "$f.tmp" "$f"
204
+ fi
205
+ done
206
+ ```
207
+
208
+ ### 3e. Clean Processing State
209
+
210
+ Remove entries from the graph_processed state so deleted files aren't
211
+ incorrectly tracked:
212
+
213
+ ```bash
214
+ # Remove processed-file entries for deleted paths
215
+ rg -v "{deleted_path}" ~/.cache/fit/basecamp/state/graph_processed \
216
+ > ~/.cache/fit/basecamp/state/graph_processed.tmp \
217
+ && mv ~/.cache/fit/basecamp/state/graph_processed.tmp \
218
+ ~/.cache/fit/basecamp/state/graph_processed
219
+ ```
220
+
221
+ ## Step 4: Write Erasure Report
222
+
223
+ Create the audit trail at `knowledge/Erasure/{Name}--{YYYY-MM-DD}.md`:
224
+
225
+ ```markdown
226
+ # Data Erasure Report — {Full Name}
227
+
228
+ **Date:** {YYYY-MM-DD HH:MM}
229
+ **Requested by:** {user or "GDPR Article 17 request"}
230
+ **Scope:** {all / recruitment-only}
231
+
232
+ ## Data Subject
233
+ - **Name:** {Full Name}
234
+ - **Known aliases:** {aliases or "none"}
235
+ - **Known emails:** {emails or "none"}
236
+
237
+ ## Actions Taken
238
+
239
+ ### Deleted Files
240
+ - `knowledge/Candidates/{Name}/brief.md`
241
+ - `knowledge/Candidates/{Name}/CV.pdf`
242
+ - `knowledge/Candidates/{Name}/assessment.md`
243
+ - `knowledge/People/{Name}.md`
244
+ - {list all deleted files}
245
+
246
+ ### Redacted References
247
+ - `knowledge/Organizations/{Agency}.md` — removed backlink
248
+ - `knowledge/Candidates/Insights.md` — removed {N} bullet(s)
249
+ - {list all redacted files and what was removed}
250
+
251
+ ### Cached Data Removed
252
+ - `~/.cache/fit/basecamp/apple_mail/{thread}.md` — deleted (sole subject)
253
+ - `~/.cache/fit/basecamp/apple_mail/{thread2}.md` — redacted (multi-person)
254
+ - {list all cache actions}
255
+
256
+ ### State Files Cleaned
257
+ - `~/.cache/fit/basecamp/state/recruiter_triage.md` — redacted
258
+ - {list all state file actions}
259
+
260
+ ## Requires Manual Action
261
+
262
+ The following data sources are outside this tool's reach:
263
+
264
+ - **Apple Mail** — original emails remain in the user's mailbox. Search for
265
+ "{Name}" in Mail.app and delete threads manually.
266
+ - **Apple Calendar** — original events remain. Check Calendar.app for events
267
+ mentioning "{Name}".
268
+ - **Recruitment agencies** — notify {Agency} that the candidate's data has been
269
+ deleted and request they do the same.
270
+ - **Interview notes** — check physical notebooks or other apps for handwritten
271
+ or external notes.
272
+ - **Shared documents** — check Google Drive, SharePoint, or other shared
273
+ platforms for documents mentioning the person.
274
+
275
+ ## Verification
276
+
277
+ After erasure, verify no traces remain:
278
+
279
+ ```bash
280
+ rg "{Name}" knowledge/ ~/.cache/fit/basecamp/
281
+ ```
282
+
283
+ Expected result: no matches (except this erasure report).
284
+ ```
285
+
286
+ **IMPORTANT:** The erasure report itself must NOT contain personal data beyond
287
+ the name and the fact that data was deleted. Do not copy CV content, skill
288
+ assessments, or candidate details into the report. Record only what was deleted,
289
+ not what it contained.
290
+
291
+ ## Step 5: Verify
292
+
293
+ Run a final search to confirm no references were missed:
294
+
295
+ ```bash
296
+ rg "{Name}" knowledge/ ~/.cache/fit/basecamp/ drafts/
297
+ ```
298
+
299
+ The only match should be the erasure report itself. If other matches remain,
300
+ process them and update the report.
301
+
302
+ ## Scope Variants
303
+
304
+ ### recruitment-only
305
+
306
+ When scope is `recruitment-only`, limit erasure to:
307
+
308
+ - `knowledge/Candidates/{Name}/` directory
309
+ - `knowledge/Candidates/Insights.md` mentions
310
+ - Recruitment-related email threads (from known agency domains)
311
+ - `recruiter_triage.md` state file
312
+
313
+ Leave `knowledge/People/{Name}.md` and general knowledge graph references
314
+ intact — the person may be a colleague or contact outside of recruitment.
315
+
316
+ ### all (default)
317
+
318
+ Full erasure across all knowledge base locations, cached data, and state files.
319
+
320
+ ## Quality Checklist
321
+
322
+ - [ ] User confirmed intent before any deletion
323
+ - [ ] Searched all data locations (knowledge, cache, state, drafts)
324
+ - [ ] All dedicated files/directories deleted
325
+ - [ ] All backlinks and mentions redacted from other notes
326
+ - [ ] Cached email threads and attachments handled
327
+ - [ ] Agent state files cleaned
328
+ - [ ] Processing state updated for deleted files
329
+ - [ ] Erasure report created with full audit trail
330
+ - [ ] Report does NOT contain personal data (only file paths and actions)
331
+ - [ ] Manual action items listed (Mail.app, Calendar.app, agencies)
332
+ - [ ] Final verification search shows no remaining references
333
+ - [ ] Broken backlinks cleaned up in referencing notes
@@ -0,0 +1,170 @@
1
+ ---
2
+ name: send-chat
3
+ description: Send messages to people via chat platforms (e.g. Microsoft Teams, Slack) using browser automation. Resolves people by name using the knowledge graph, drafts messages for approval, and sends via the web app. Use when the user asks to message, ping, or chat with someone.
4
+ compatibility:
5
+ requires:
6
+ - browser-automation
7
+ ---
8
+
9
+ # Send Chat
10
+
11
+ Send chat messages to people using browser automation against a web-based chat
12
+ platform (Microsoft Teams, Slack, or similar). Resolves recipients by name from
13
+ the knowledge graph so the user can say "message Sarah about the standup"
14
+ without needing exact display names.
15
+
16
+ ## Trigger
17
+
18
+ Run when the user asks to:
19
+
20
+ - Send a message on Teams / Slack / chat
21
+ - Ping / chat / DM someone
22
+ - Follow up with someone via chat
23
+ - Send a message about a topic
24
+
25
+ ## Prerequisites
26
+
27
+ - Chat platform web app open and authenticated in the browser
28
+ - Browser automation available (e.g. Chrome MCP, Playwright)
29
+ - Knowledge base populated with people notes
30
+
31
+ ## Critical: Always Look Up Context First
32
+
33
+ **BEFORE messaging anyone, you MUST look up the person in the knowledge base.**
34
+
35
+ When the user mentions ANY person:
36
+
37
+ 1. **STOP** — Do not open the chat platform yet
38
+ 2. **SEARCH** — Look them up: `rg -l "{name}" knowledge/People/`
39
+ 3. **READ** — Read their note to understand context, role, recent interactions
40
+ 4. **UNDERSTAND** — Know who they are, what you've been working on together
41
+ 5. **THEN PROCEED** — Only now compose the message and use browser automation
42
+
43
+ This context is essential for:
44
+
45
+ - Finding the right person if the name is ambiguous
46
+ - Drafting an appropriate message if the user gave a loose prompt
47
+ - Knowing the person's role and relationship for tone
48
+
49
+ ## Resolving People
50
+
51
+ The user will refer to people by first name, last name, or nickname. Resolve to
52
+ a full name using the knowledge graph:
53
+
54
+ ```bash
55
+ # Find person by partial name
56
+ rg -l -i "{name}" knowledge/People/
57
+
58
+ # If ambiguous, read candidates to disambiguate
59
+ cat "knowledge/People/{Candidate}.md"
60
+ ```
61
+
62
+ **If ambiguous** (multiple matches), ask the user which person they mean — list
63
+ the matches with roles/orgs to help them pick.
64
+
65
+ **If no match**, tell the user you don't have this person in the knowledge base
66
+ and ask for their full name as it appears in the chat platform.
67
+
68
+ ## Composing the Message
69
+
70
+ **Every message MUST be drafted as a text file first.** This ensures the user
71
+ can review and edit the exact message before it's sent.
72
+
73
+ ### Draft Workflow
74
+
75
+ 1. **Compose the message** based on context and user intent.
76
+ 2. **Write it to a draft file** at `drafts/chat-{recipient-slug}-{date}.md`
77
+ - `{recipient-slug}` = lowercase, hyphenated full name (e.g. `sarah-chen`)
78
+ - `{date}` = ISO date (e.g. `2026-02-19`)
79
+ 3. **Show the user the draft** — display the file path and contents.
80
+ 4. **Wait for approval** — the user may edit the file or ask for changes.
81
+ 5. **Only after approval**, proceed to send.
82
+
83
+ **Draft file format:**
84
+
85
+ ```markdown
86
+ To: {Full Name}
87
+ Via: {Platform name}
88
+ Date: {YYYY-MM-DD}
89
+
90
+ ---
91
+
92
+ {message body}
93
+ ```
94
+
95
+ The message body (everything below the `---` separator) is what gets pasted into
96
+ the chat.
97
+
98
+ **Message guidelines:**
99
+
100
+ - Match the user's usual tone — casual for peers, professional for leadership
101
+ - Keep it concise — chat is informal, not email
102
+ - Reference specific context naturally (project names, recent decisions)
103
+ - If the user provides exact wording, use it verbatim
104
+ - If the user said "ping {name}" without detail, ask what they want to say
105
+ - Draft one message based on context — don't offer multiple options
106
+ - **Keep messages on a single line with no formatting.** No line breaks, no
107
+ markdown. Use inline separators (e.g. `•`, `—`) to keep structure. Multi-line
108
+ formatting is unreliable via browser automation.
109
+
110
+ ## Browser Automation Flow
111
+
112
+ Once the user has approved the draft, send it as a **single submission** — paste
113
+ the entire message at once rather than typing line by line.
114
+
115
+ ### Step 1: Identify the Chat Platform
116
+
117
+ Check which platform is available:
118
+
119
+ - Look for an open tab matching the configured chat URL
120
+ - If no tab is open, ask the user which platform to use and navigate to it
121
+
122
+ ### Step 2: Open a Chat with the Recipient
123
+
124
+ 1. Use the platform's search or "New chat" feature
125
+ 2. Type the recipient's full name
126
+ 3. Wait for search results to populate (take a screenshot to verify)
127
+ 4. Click the correct person from the results
128
+
129
+ If the person doesn't appear in search, inform the user — they may not be in the
130
+ same organization.
131
+
132
+ ### Step 3: Send the Approved Message
133
+
134
+ 1. Read the approved draft file to get the message body (below the `---`)
135
+ 2. Click the message compose box
136
+ 3. Paste the entire message as a single submission
137
+ 4. Press Enter or click Send
138
+ 5. Take a screenshot to confirm the message was sent
139
+
140
+ ### Step 4: Update Knowledge Graph (Optional)
141
+
142
+ If the message is substantive (not just "hey" or "thanks"), note the interaction
143
+ on the person's knowledge note:
144
+
145
+ ```markdown
146
+ - {YYYY-MM-DD}: Messaged on {Platform} re: {topic}
147
+ ```
148
+
149
+ ## Error Handling
150
+
151
+ - **Platform not loaded / auth required:** Tell the user to sign in first, then
152
+ retry
153
+ - **Person not found in search:** Report back — they may be external or using a
154
+ different display name. Ask the user for the exact name
155
+ - **Chat already open:** If a chat with this person is already visible, use it
156
+ directly
157
+ - **UI not as expected:** Take a screenshot and describe what you see. Don't
158
+ click blindly
159
+
160
+ ## Constraints
161
+
162
+ - **Always confirm before sending.** Never send a message without explicit user
163
+ approval — this is a hard requirement
164
+ - **One message at a time.** Don't batch-send to multiple people without
165
+ confirming each one
166
+ - **No file attachments.** This skill handles text messages only
167
+ - **No group chats.** Targets 1:1 chats only
168
+ - **No message deletion or editing.** Once sent, it's sent
169
+ - **Respect ethics rules.** Never send messages that contain personal judgments,
170
+ gossip, or sensitive information per the knowledge base ethics policy
@@ -37,10 +37,11 @@ their calendar.
37
37
 
38
38
  ## Implementation
39
39
 
40
- Run the sync as a single Python script. This avoids N+1 sqlite3 invocations (one
41
- per event for attendees) and handles all data transformation in one pass:
40
+ Run the sync as a single Node.js script with embedded SQLite. This avoids N+1
41
+ process invocations (one per event for attendees) and handles all data
42
+ transformation in one pass:
42
43
 
43
- python3 scripts/sync.py [--days N]
44
+ node scripts/sync.mjs [--days N]
44
45
 
45
46
  - `--days N` — how many days back to sync (default: 30)
46
47
 
@@ -97,8 +98,7 @@ Each `{event_id}.json` file:
97
98
 
98
99
  ## Constraints
99
100
 
100
- - Open database read-only (`-readonly`)
101
+ - Open database read-only (`readOnly: true`)
101
102
  - This sync is stateless — always queries the current sliding window
102
103
  - All-day events may have null end times — use start date as end date
103
104
  - All-day events have timezone `_float` — omit timezone from output
104
- - Output format matches Google Calendar event format for downstream consistency