@forwardimpact/basecamp 0.1.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 (36) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +229 -0
  3. package/build.js +124 -0
  4. package/config/scheduler.json +28 -0
  5. package/package.json +37 -0
  6. package/scheduler.js +552 -0
  7. package/scripts/build-pkg.sh +117 -0
  8. package/scripts/compile.sh +26 -0
  9. package/scripts/install.sh +108 -0
  10. package/scripts/pkg-resources/conclusion.html +62 -0
  11. package/scripts/pkg-resources/welcome.html +64 -0
  12. package/scripts/postinstall +46 -0
  13. package/scripts/uninstall.sh +56 -0
  14. package/template/.claude/settings.json +40 -0
  15. package/template/.claude/skills/create-presentations/SKILL.md +75 -0
  16. package/template/.claude/skills/create-presentations/references/slide.css +35 -0
  17. package/template/.claude/skills/create-presentations/scripts/convert-to-pdf.js +32 -0
  18. package/template/.claude/skills/doc-collab/SKILL.md +112 -0
  19. package/template/.claude/skills/draft-emails/SKILL.md +191 -0
  20. package/template/.claude/skills/draft-emails/scripts/scan-emails.sh +33 -0
  21. package/template/.claude/skills/extract-entities/SKILL.md +466 -0
  22. package/template/.claude/skills/extract-entities/references/TEMPLATES.md +131 -0
  23. package/template/.claude/skills/extract-entities/scripts/state.py +100 -0
  24. package/template/.claude/skills/meeting-prep/SKILL.md +135 -0
  25. package/template/.claude/skills/organize-files/SKILL.md +146 -0
  26. package/template/.claude/skills/organize-files/scripts/organize-by-type.sh +42 -0
  27. package/template/.claude/skills/organize-files/scripts/summarize.sh +21 -0
  28. package/template/.claude/skills/sync-apple-calendar/SKILL.md +101 -0
  29. package/template/.claude/skills/sync-apple-calendar/references/SCHEMA.md +80 -0
  30. package/template/.claude/skills/sync-apple-calendar/scripts/sync.py +233 -0
  31. package/template/.claude/skills/sync-apple-mail/SKILL.md +131 -0
  32. package/template/.claude/skills/sync-apple-mail/references/SCHEMA.md +88 -0
  33. package/template/.claude/skills/sync-apple-mail/scripts/parse-emlx.py +104 -0
  34. package/template/.claude/skills/sync-apple-mail/scripts/sync.py +348 -0
  35. package/template/CLAUDE.md +152 -0
  36. package/template/USER.md +5 -0
@@ -0,0 +1,131 @@
1
+ # Note Templates
2
+
3
+ Templates for creating new knowledge base notes.
4
+
5
+ ## People
6
+
7
+ ```markdown
8
+ # {Full Name}
9
+
10
+ ## Info
11
+ **Role:** {role or inferred role with qualifier}
12
+ **Organization:** [[Organizations/{organization}]]
13
+ **Email:** {email}
14
+ **Aliases:** {comma-separated variants}
15
+ **First met:** {YYYY-MM-DD}
16
+ **Last seen:** {YYYY-MM-DD}
17
+
18
+ ## Summary
19
+ {2-3 sentences: who they are, why you know them, what you're working on}
20
+
21
+ ## Connected to
22
+ - [[Organizations/{Org}]] — works at
23
+ - [[People/{Person}]] — {relationship}
24
+ - [[Projects/{Project}]] — {role}
25
+
26
+ ## Activity
27
+ - **{YYYY-MM-DD}** ({meeting|email|voice memo}): {Summary with [[Folder/Name]] links}
28
+
29
+ ## Key facts
30
+ {substantive facts only — leave empty if none}
31
+
32
+ ## Open items
33
+ {commitments and next steps only — leave empty if none}
34
+ ```
35
+
36
+ ## Organizations
37
+
38
+ ```markdown
39
+ # {Organization Name}
40
+
41
+ ## Info
42
+ **Type:** {company|team|institution}
43
+ **Industry:** {industry}
44
+ **Relationship:** {customer|prospect|partner|vendor}
45
+ **Domain:** {primary email domain}
46
+ **Aliases:** {comma-separated}
47
+ **First met:** {YYYY-MM-DD}
48
+ **Last seen:** {YYYY-MM-DD}
49
+
50
+ ## Summary
51
+ {2-3 sentences}
52
+
53
+ ## People
54
+ - [[People/{Person}]] — {role}
55
+
56
+ ## Contacts
57
+ {for transactional contacts who don't get their own notes}
58
+
59
+ ## Projects
60
+ - [[Projects/{Project}]] — {relationship}
61
+
62
+ ## Activity
63
+ - **{YYYY-MM-DD}** ({type}): {Summary}
64
+
65
+ ## Key facts
66
+
67
+ ## Open items
68
+ ```
69
+
70
+ ## Projects
71
+
72
+ ```markdown
73
+ # {Project Name}
74
+
75
+ ## Info
76
+ **Type:** {deal|product|initiative|hiring}
77
+ **Status:** {active|planning|on hold|completed|cancelled}
78
+ **Started:** {YYYY-MM-DD}
79
+ **Last activity:** {YYYY-MM-DD}
80
+
81
+ ## Summary
82
+ {2-3 sentences}
83
+
84
+ ## People
85
+ - [[People/{Person}]] — {role}
86
+
87
+ ## Organizations
88
+ - [[Organizations/{Org}]] — {relationship}
89
+
90
+ ## Related
91
+ - [[Topics/{Topic}]] — {relationship}
92
+
93
+ ## Timeline
94
+ **{YYYY-MM-DD}** ({type})
95
+ {What happened with [[links]]}
96
+
97
+ ## Decisions
98
+ - **{YYYY-MM-DD}**: {Decision}. {Rationale}.
99
+
100
+ ## Open items
101
+
102
+ ## Key facts
103
+ ```
104
+
105
+ ## Topics
106
+
107
+ ```markdown
108
+ # {Topic Name}
109
+
110
+ ## About
111
+ {1-2 sentences}
112
+
113
+ **Keywords:** {comma-separated}
114
+ **Aliases:** {other references}
115
+ **First mentioned:** {YYYY-MM-DD}
116
+ **Last mentioned:** {YYYY-MM-DD}
117
+
118
+ ## Related
119
+ - [[People/{Person}]] — {relationship}
120
+ - [[Organizations/{Org}]] — {relationship}
121
+
122
+ ## Log
123
+ **{YYYY-MM-DD}** ({type}: {title})
124
+ {Summary with [[links]]}
125
+
126
+ ## Decisions
127
+
128
+ ## Open items
129
+
130
+ ## Key facts
131
+ ```
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env python3
2
+ """Manage graph_processed state for entity extraction.
3
+
4
+ Commands:
5
+ check - Find unprocessed or changed source files
6
+ update - Mark a file as processed (updates its hash in state)
7
+
8
+ Usage:
9
+ python3 scripts/state.py check # List new/changed files
10
+ python3 scripts/state.py update <file-path> # Mark file as processed
11
+ python3 scripts/state.py update <file1> <file2> … # Mark multiple files
12
+
13
+ State file: ~/.cache/fit/basecamp/state/graph_processed (TSV: path<TAB>hash)
14
+ """
15
+
16
+ import hashlib
17
+ import os
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ STATE_FILE = Path.home() / ".cache/fit/basecamp/state/graph_processed"
22
+ SOURCE_DIRS = [
23
+ Path.home() / ".cache/fit/basecamp/apple_mail",
24
+ Path.home() / ".cache/fit/basecamp/apple_calendar",
25
+ ]
26
+
27
+
28
+ def file_hash(path):
29
+ """Compute SHA-256 hash of a file."""
30
+ h = hashlib.sha256()
31
+ with open(path, "rb") as f:
32
+ for chunk in iter(lambda: f.read(8192), b""):
33
+ h.update(chunk)
34
+ return h.hexdigest()
35
+
36
+
37
+ def load_state():
38
+ """Load the state file into a dict of {path: hash}."""
39
+ state = {}
40
+ if STATE_FILE.exists():
41
+ for line in STATE_FILE.read_text().splitlines():
42
+ parts = line.split("\t", 1)
43
+ if len(parts) == 2:
44
+ state[parts[0]] = parts[1]
45
+ return state
46
+
47
+
48
+ def save_state(state):
49
+ """Write the full state dict back to the state file."""
50
+ STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
51
+ lines = [f"{path}\t{h}" for path, h in sorted(state.items())]
52
+ STATE_FILE.write_text("\n".join(lines) + "\n" if lines else "")
53
+
54
+
55
+ def check():
56
+ """Find source files that are new or have changed since last processing."""
57
+ state = load_state()
58
+ new_files = []
59
+ for source_dir in SOURCE_DIRS:
60
+ if not source_dir.is_dir():
61
+ continue
62
+ for f in source_dir.iterdir():
63
+ if not f.is_file():
64
+ continue
65
+ path_str = str(f)
66
+ h = file_hash(f)
67
+ if state.get(path_str) != h:
68
+ new_files.append(path_str)
69
+ for f in sorted(new_files):
70
+ print(f)
71
+ return len(new_files)
72
+
73
+
74
+ def update(file_paths):
75
+ """Mark files as processed by updating their hashes in state."""
76
+ state = load_state()
77
+ for fp in file_paths:
78
+ p = Path(fp)
79
+ if not p.exists():
80
+ print(f"Warning: File not found: {fp}", file=sys.stderr)
81
+ continue
82
+ state[str(p)] = file_hash(p)
83
+ save_state(state)
84
+ print(f"Updated {len(file_paths)} file(s) in graph state")
85
+
86
+
87
+ if __name__ == "__main__":
88
+ if len(sys.argv) < 2:
89
+ print(__doc__, file=sys.stderr)
90
+ sys.exit(1)
91
+
92
+ cmd = sys.argv[1]
93
+ if cmd == "check":
94
+ count = check()
95
+ print(f"\n{count} file(s) to process", file=sys.stderr)
96
+ elif cmd == "update" and len(sys.argv) >= 3:
97
+ update(sys.argv[2:])
98
+ else:
99
+ print(__doc__, file=sys.stderr)
100
+ sys.exit(1)
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: meeting-prep
3
+ description: Prepare for meetings by gathering context from the knowledge base and calendar. Use when the user asks to prep for a meeting or wants a briefing on upcoming meetings. Creates personalized briefings with attendee history, open items, and suggested talking points.
4
+ ---
5
+
6
+ # Meeting Prep
7
+
8
+ Help the user prepare for meetings by gathering context from the knowledge base
9
+ and calendar. Creates personalized briefing documents with attendee history,
10
+ open items, and suggested talking points.
11
+
12
+ ## Trigger
13
+
14
+ Run when the user asks to prep for a meeting, or wants a briefing on upcoming
15
+ meetings.
16
+
17
+ ## Prerequisites
18
+
19
+ - Calendar data synced in `~/.cache/fit/basecamp/apple_calendar/`
20
+ - Knowledge base populated (from `extract-entities` skill)
21
+
22
+ ## Inputs
23
+
24
+ - `~/.cache/fit/basecamp/apple_calendar/*.json` — calendar events
25
+ - `knowledge/People/*.md` — attendee context
26
+ - `knowledge/Organizations/*.md` — company context
27
+ - `knowledge/Projects/*.md` — project context
28
+
29
+ ## Outputs
30
+
31
+ - Meeting brief printed to the user (not saved to a file)
32
+
33
+ ---
34
+
35
+ ## Critical: Always Look Up Context First
36
+
37
+ **BEFORE creating any meeting brief, you MUST look up the attendees in the
38
+ knowledge base.**
39
+
40
+ When the user asks to prep for a meeting:
41
+
42
+ 1. **STOP** — Do not create a generic brief
43
+ 2. **SEARCH** — Look up each attendee: `rg -l "Attendee Name" knowledge/`
44
+ 3. **READ** — Read their notes: `cat "knowledge/People/Attendee Name.md"`
45
+ 4. **UNDERSTAND** — Extract role, organization, history, open items
46
+ 5. **THEN BRIEF** — Create the meeting brief using this context
47
+
48
+ ## Key Principles
49
+
50
+ **Ask, don't guess:**
51
+
52
+ - If unclear which meeting, ASK
53
+ - If multiple upcoming meetings, offer choices
54
+ - **WRONG:** "Here's a generic meeting prep template"
55
+ - **RIGHT:** "I see meetings with Sarah (2pm) and John (4pm). Which one?"
56
+
57
+ **Be thorough, not generic:**
58
+
59
+ - Include specific history, open items, and context from knowledge base
60
+ - Reference actual past interactions and commitments
61
+
62
+ ## Processing Flow
63
+
64
+ ### Step 1: Identify the Meeting
65
+
66
+ If specified, look it up in calendar:
67
+
68
+ ```bash
69
+ ls ~/.cache/fit/basecamp/apple_calendar/ 2>/dev/null
70
+ cat "$HOME/.cache/fit/basecamp/apple_calendar/event123.json"
71
+ ```
72
+
73
+ If "prep me for my next meeting":
74
+
75
+ - List upcoming events
76
+ - Find the next meeting with external attendees
77
+ - Confirm with user if unclear
78
+
79
+ ### Step 2: Parse Calendar Event
80
+
81
+ Extract: summary, start/end time, attendees (names and emails), description.
82
+
83
+ ### Step 3: Gather Context from Knowledge Base
84
+
85
+ For each attendee:
86
+
87
+ ```bash
88
+ rg -l "attendee_name" knowledge/People/
89
+ rg -l "attendee_email" knowledge/People/
90
+ cat "knowledge/People/Attendee Name.md"
91
+ cat "knowledge/Organizations/Their Company.md"
92
+ rg -l "attendee_name" knowledge/Projects/
93
+ ```
94
+
95
+ Extract: role/title, company, key facts, previous interactions, open items.
96
+
97
+ ### Step 4: Create Meeting Brief
98
+
99
+ Format:
100
+
101
+ ```markdown
102
+ Meeting Brief: {Attendee Name}
103
+ {Time} today / {Company}
104
+
105
+ About {First Name}
106
+ {Role at company}. {Key background — 1-2 sentences}. {What they focus on}.
107
+
108
+ Your History
109
+ - {Date}: {Brief description of interaction/outcome}
110
+ - {Date}: {Brief description}
111
+ - {Date}: {Brief description}
112
+
113
+ Open Items
114
+ - {Action item} (they asked {date})
115
+ - {Action item}
116
+
117
+ Suggested Talking Points
118
+ - {Concrete suggestion based on history}
119
+ - {Reference relevant entities with [[wiki-links]]}
120
+ ```
121
+
122
+ **Guidelines:**
123
+
124
+ - Use `[[Name]]` wiki-link syntax for cross-references
125
+ - Keep "About" section to 2-3 sentences max
126
+ - History: reverse chronological, 3-5 most relevant items
127
+ - Talking points: concrete, not generic
128
+ - If no notes exist for a person, mention that and offer to create one
129
+
130
+ ## Constraints
131
+
132
+ - Only prep for meetings with external attendees
133
+ - Skip internal calendar blocks (DND, Focus Time, Lunch)
134
+ - For meetings with multiple attendees, create sections for each key person
135
+ - Prioritize recent interactions (last 30 days)
@@ -0,0 +1,146 @@
1
+ ---
2
+ name: organize-files
3
+ description: Organize, tidy up, and find files in ~/Desktop/ and ~/Downloads/. Use when the user asks to find, organize, clean up, or tidy files on their Mac. Always previews changes before acting and never deletes without explicit confirmation. Extracts entities from document files using the extract-entities skill.
4
+ compatibility: Requires macOS filesystem access
5
+ ---
6
+
7
+ # Organize Files
8
+
9
+ Organize, tidy up, and find files in `~/Desktop/` and `~/Downloads/`. Always
10
+ previews changes before acting and never deletes without explicit confirmation.
11
+
12
+ After organizing, extract entities from document files by invoking the
13
+ **`extract-entities`** skill.
14
+
15
+ ## Trigger
16
+
17
+ Run when the user asks to find, organize, clean up, or tidy files on their Mac.
18
+
19
+ ## Prerequisites
20
+
21
+ - macOS filesystem access
22
+
23
+ ## Inputs
24
+
25
+ - User's description of what to organize or find
26
+ - Source directories: `~/Desktop/` and `~/Downloads/`
27
+
28
+ ## Outputs
29
+
30
+ - Organized files moved to logical subdirectories within `~/Desktop/` and
31
+ `~/Downloads/`
32
+ - Entity extraction triggered on document files via the **`extract-entities`**
33
+ skill
34
+ - Summary of actions taken
35
+
36
+ ---
37
+
38
+ ## Core Capabilities
39
+
40
+ 1. **Find files** — Locate files by name, type, or content in `~/Desktop/` and
41
+ `~/Downloads/`
42
+ 2. **Organize files** — Move files into logical subfolders
43
+ 3. **Tidy up** — Clean up cluttered `~/Desktop/` and `~/Downloads/`
44
+ 4. **Create structure** — Set up folder hierarchies
45
+ 5. **Extract entities** — After organizing, invoke the **`extract-entities`**
46
+ skill on document files to populate the knowledge graph
47
+
48
+ ## Key Principles
49
+
50
+ **Always preview before acting:**
51
+
52
+ - Show what files will be affected BEFORE moving/deleting
53
+ - List proposed changes and ask for confirmation
54
+
55
+ **Be conservative with destructive operations:**
56
+
57
+ - Never delete without explicit confirmation
58
+ - Prefer moving to a "to-review" folder over deleting
59
+
60
+ ## Summarizing Contents
61
+
62
+ Get an overview of both directories:
63
+
64
+ bash scripts/summarize.sh
65
+
66
+ ## Finding Files
67
+
68
+ ```bash
69
+ find ~/Downloads -maxdepth 1 -name "*.pdf" -type f
70
+ find ~/Desktop -maxdepth 1 -iname "*AI*" -type f
71
+ find ~/Desktop -maxdepth 1 -type f \( -name "*.png" -o -name "*.jpg" \)
72
+ find ~/Downloads -maxdepth 1 -type f -mtime -7 # last 7 days
73
+ find ~/Downloads -maxdepth 1 -type f -mtime +30 # older than 30 days
74
+ find ~/Desktop -maxdepth 1 \( -name "Screenshot*" -o -name "Screen Shot*" \)
75
+ ```
76
+
77
+ ## Organizing by File Type
78
+
79
+ Organize a directory into type-based subdirectories (Documents, Images,
80
+ Archives, Installers, Screenshots):
81
+
82
+ bash scripts/organize-by-type.sh ~/Downloads
83
+ bash scripts/organize-by-type.sh ~/Desktop
84
+
85
+ The script creates subdirectories and moves matching files. It does NOT delete
86
+ anything.
87
+
88
+ ## Entity Extraction
89
+
90
+ After organizing files, identify document files that may contain entity
91
+ information (people, organizations, projects, topics) and invoke the
92
+ **`extract-entities`** skill to process them.
93
+
94
+ ### Which files to send for extraction
95
+
96
+ **Include:** `.pdf`, `.txt`, `.md`, `.rtf`, `.doc`, `.docx`, `.csv`, `.xlsx`
97
+
98
+ **Exclude:** Images, installers, archives, media, system files
99
+
100
+ ### How to invoke
101
+
102
+ After organizing, collect the paths of document files and invoke the
103
+ **`extract-entities`** skill, passing the file paths as ad-hoc file inputs.
104
+
105
+ ## Output Format
106
+
107
+ **Plan:**
108
+
109
+ ```
110
+ Organization Plan: Desktop & Downloads Cleanup
111
+
112
+ Found 47 files to organize:
113
+ - 23 screenshots → ~/Desktop/Screenshots/
114
+ - 12 PDFs → ~/Downloads/Documents/
115
+ - 8 images → ~/Downloads/Images/
116
+ - 4 DMGs → ~/Downloads/Installers/
117
+
118
+ Document files for entity extraction: 12
119
+ → Will invoke extract-entities skill after organizing
120
+
121
+ Should I proceed?
122
+ ```
123
+
124
+ **Results:**
125
+
126
+ ```
127
+ Organization Complete
128
+
129
+ Moved 47 files:
130
+ - 23 screenshots to ~/Desktop/Screenshots/
131
+ - 12 PDFs to ~/Downloads/Documents/
132
+ - 8 images to ~/Downloads/Images/
133
+ - 4 DMGs to ~/Downloads/Installers/
134
+
135
+ Entity extraction: invoked extract-entities on 12 document files
136
+ ```
137
+
138
+ ## Safety Rules
139
+
140
+ 1. **Never delete without permission** — "cleanup" means organize, not delete
141
+ 2. **Don't touch system folders** — /System, /Library, /Applications
142
+ 3. **Don't touch hidden files** — files starting with `.` unless asked
143
+ 4. **Limit scope** — only operate on `~/Desktop/` and `~/Downloads/`
144
+ 5. **Limit depth** — use `-maxdepth 1` unless user wants recursive
145
+ 6. **Show before doing** — always preview first
146
+ 7. **Quote paths** — handle spaces: `"$HOME/My Documents"`
@@ -0,0 +1,42 @@
1
+ #!/bin/bash
2
+ # Organize files in a directory by type into subdirectories.
3
+ #
4
+ # Usage: bash scripts/organize-by-type.sh <directory>
5
+ #
6
+ # Creates subdirectories (Documents, Images, Archives, Installers, Screenshots)
7
+ # and moves matching files. Only operates on top-level files (-maxdepth 1).
8
+ # Prints each move with -v flag. Does NOT delete any files.
9
+
10
+ set -euo pipefail
11
+
12
+ if [ $# -ne 1 ]; then
13
+ echo "Usage: bash scripts/organize-by-type.sh <directory>" >&2
14
+ exit 1
15
+ fi
16
+
17
+ DIR="$1"
18
+
19
+ if [ ! -d "$DIR" ]; then
20
+ echo "Error: Directory not found: $DIR" >&2
21
+ exit 1
22
+ fi
23
+
24
+ # Create subdirectories
25
+ mkdir -p "$DIR"/{Documents,Images,Archives,Installers,Screenshots}
26
+
27
+ # Screenshots
28
+ find "$DIR" -maxdepth 1 -type f \( -name "Screenshot*" -o -name "Screen Shot*" \) -exec mv -v {} "$DIR/Screenshots/" \;
29
+
30
+ # Documents
31
+ find "$DIR" -maxdepth 1 -type f \( -name "*.pdf" -o -name "*.doc*" -o -name "*.txt" -o -name "*.md" -o -name "*.rtf" -o -name "*.csv" -o -name "*.xlsx" \) -exec mv -v {} "$DIR/Documents/" \;
32
+
33
+ # Images (excluding screenshots already moved)
34
+ find "$DIR" -maxdepth 1 -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.gif" -o -name "*.webp" \) -exec mv -v {} "$DIR/Images/" \;
35
+
36
+ # Archives
37
+ find "$DIR" -maxdepth 1 -type f \( -name "*.zip" -o -name "*.tar.gz" -o -name "*.rar" \) -exec mv -v {} "$DIR/Archives/" \;
38
+
39
+ # Installers
40
+ find "$DIR" -maxdepth 1 -type f -name "*.dmg" -exec mv -v {} "$DIR/Installers/" \;
41
+
42
+ echo "Organization complete: $DIR"
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+ # Summarize the contents of ~/Desktop/ and ~/Downloads/.
3
+ #
4
+ # Usage: bash scripts/summarize.sh
5
+ #
6
+ # Counts files by type in both directories (top-level only).
7
+
8
+ set -euo pipefail
9
+
10
+ for dir in "$HOME/Desktop" "$HOME/Downloads"; do
11
+ [ -d "$dir" ] || continue
12
+ echo "=== $(basename "$dir") ==="
13
+ echo "Screenshots: $(find "$dir" -maxdepth 1 \( -name 'Screenshot*' -o -name 'Screen Shot*' \) 2>/dev/null | wc -l | tr -d ' ')"
14
+ echo "PDFs: $(find "$dir" -maxdepth 1 -name '*.pdf' 2>/dev/null | wc -l | tr -d ' ')"
15
+ echo "Images: $(find "$dir" -maxdepth 1 \( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' -o -name '*.gif' -o -name '*.webp' \) 2>/dev/null | wc -l | tr -d ' ')"
16
+ echo "Documents: $(find "$dir" -maxdepth 1 \( -name '*.doc*' -o -name '*.txt' -o -name '*.md' -o -name '*.rtf' \) 2>/dev/null | wc -l | tr -d ' ')"
17
+ echo "Archives: $(find "$dir" -maxdepth 1 \( -name '*.zip' -o -name '*.tar.gz' -o -name '*.rar' \) 2>/dev/null | wc -l | tr -d ' ')"
18
+ echo "Installers: $(find "$dir" -maxdepth 1 -name '*.dmg' 2>/dev/null | wc -l | tr -d ' ')"
19
+ echo "Other: $(find "$dir" -maxdepth 1 -type f ! \( -name 'Screenshot*' -o -name 'Screen Shot*' -o -name '*.pdf' -o -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' -o -name '*.gif' -o -name '*.webp' -o -name '*.doc*' -o -name '*.txt' -o -name '*.md' -o -name '*.rtf' -o -name '*.zip' -o -name '*.tar.gz' -o -name '*.rar' -o -name '*.dmg' -o -name '.DS_Store' -o -name '.localized' \) 2>/dev/null | wc -l | tr -d ' ')"
20
+ echo ""
21
+ done
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: sync-apple-calendar
3
+ description: Sync calendar events from the macOS Calendar app's local SQLite database into ~/.cache/fit/basecamp/apple_calendar/ as JSON files. Use on a schedule or when the user asks to sync their calendar. Requires macOS with Calendar app configured and Full Disk Access granted.
4
+ compatibility: Requires macOS with Apple Calendar configured and Full Disk Access granted to the terminal
5
+ ---
6
+
7
+ # Sync Apple Calendar
8
+
9
+ Sync calendar events from the macOS Calendar app's local SQLite database into
10
+ `~/.cache/fit/basecamp/apple_calendar/` as JSON files. This is an automated data
11
+ pipeline skill — it ingests raw calendar data that other skills (like
12
+ `extract-entities` and `meeting-prep`) consume downstream.
13
+
14
+ ## Trigger
15
+
16
+ Run this skill on a schedule (every 5 minutes) or when the user asks to sync
17
+ their calendar.
18
+
19
+ ## Prerequisites
20
+
21
+ - macOS with the built-in Calendar app configured
22
+ - Full Disk Access granted to the terminal (System Settings → Privacy & Security
23
+ → Full Disk Access)
24
+
25
+ ## Inputs
26
+
27
+ - `~/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb` —
28
+ Apple Calendar SQLite database (Sonoma+/macOS 14+)
29
+ - `~/Library/Calendars/Calendar.sqlitedb` — fallback path for older macOS
30
+
31
+ ## Outputs
32
+
33
+ - `~/.cache/fit/basecamp/apple_calendar/{event_id}.json` — one JSON file per
34
+ event (14-day sliding window)
35
+
36
+ ---
37
+
38
+ ## Implementation
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:
42
+
43
+ python3 scripts/sync.py
44
+
45
+ The script:
46
+
47
+ 1. Finds the Calendar database (Sonoma+ path first, then fallback)
48
+ 2. Queries all events in a 14-day past/future window with a single SQL query
49
+ 3. Batch-fetches all attendees for those events in one query
50
+ 4. Writes one JSON file per event to `~/.cache/fit/basecamp/apple_calendar/`
51
+ 5. Cleans up JSON files for events now outside the window
52
+ 6. Reports summary (events synced, files cleaned up)
53
+
54
+ ## Database Schema
55
+
56
+ See [references/SCHEMA.md](references/SCHEMA.md) for the complete Apple Calendar
57
+ SQLite schema including table structures, column names, and important caveats
58
+ (e.g., Identity uses `address` not `email`, Participant has no `display_name`).
59
+
60
+ ## Output Format
61
+
62
+ Each `{event_id}.json` file:
63
+
64
+ ```json
65
+ {
66
+ "id": "apple_cal_123",
67
+ "summary": "Meeting with Sarah Chen",
68
+ "start": { "dateTime": "2025-02-12T15:00:00+01:00", "timeZone": "Europe/Paris" },
69
+ "end": { "dateTime": "2025-02-12T16:00:00+01:00", "timeZone": "Europe/Paris" },
70
+ "allDay": false,
71
+ "location": "Zoom",
72
+ "description": "Discuss Q2 roadmap",
73
+ "conferenceUrl": null,
74
+ "calendar": "Calendar",
75
+ "organizer": { "email": "sarah@acme.com", "name": "Chen, Sarah" },
76
+ "attendees": [
77
+ {
78
+ "email": "sarah@acme.com",
79
+ "name": "Chen, Sarah",
80
+ "status": "accepted",
81
+ "role": "required",
82
+ "self": false
83
+ }
84
+ ]
85
+ }
86
+ ```
87
+
88
+ ## Error Handling
89
+
90
+ - Database not found → Calendar not configured, report and stop
91
+ - Permission denied → Full Disk Access not granted, report and stop
92
+ - Database locked → wait 2 seconds, retry once
93
+ - Skip events with no summary (likely cancelled or placeholder)
94
+
95
+ ## Constraints
96
+
97
+ - Open database read-only (`-readonly`)
98
+ - This sync is stateless — always queries the current 14-day window
99
+ - All-day events may have null end times — use start date as end date
100
+ - All-day events have timezone `_float` — omit timezone from output
101
+ - Output format matches Google Calendar event format for downstream consistency