@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,250 @@
1
+ ---
2
+ name: weekly-update
3
+ description: Generate or refresh a weekly priorities document. Pulls from task boards, recent activity, and calendar to create a point-in-time snapshot. Use when the user asks to update their weekly, prep for the week, or on a Monday morning schedule.
4
+ ---
5
+
6
+ # Weekly Update
7
+
8
+ Generate or refresh a weekly priorities document in
9
+ `knowledge/Weeklies/{Person Name}/`. Each weekly is a point-in-time snapshot —
10
+ created at the start of the week, updated as priorities shift, and closed out
11
+ with a retrospective at end-of-week.
12
+
13
+ ## Trigger
14
+
15
+ Run this skill:
16
+
17
+ - When the user asks to update their weekly or prep for the week
18
+ - Monday mornings (scheduled)
19
+ - When the user asks to review someone's weekly
20
+ - End-of-week to fill in accomplishments and retrospective
21
+
22
+ ## Prerequisites
23
+
24
+ - `knowledge/Weeklies/` directory exists
25
+ - `knowledge/Tasks/{Person Name}.md` exists (task board — see `manage-tasks`
26
+ skill)
27
+ - Calendar data in `~/.cache/fit/basecamp/apple_calendar/`
28
+ - User identity configured in `USER.md`
29
+
30
+ ## Inputs
31
+
32
+ - `knowledge/Tasks/{Person Name}.md` — current task board
33
+ - `knowledge/People/{Person Name}.md` — recent activity, context
34
+ - `~/.cache/fit/basecamp/apple_calendar/*.json` — calendar events for the week
35
+ - Previous weekly document (if exists) — for continuity and carry-forward
36
+
37
+ ## Outputs
38
+
39
+ - `knowledge/Weeklies/{Person Name}/{YYYY}-W{WW}.md` — the weekly document
40
+
41
+ ---
42
+
43
+ ## Weekly Document Format
44
+
45
+ ```markdown
46
+ # {YYYY}-W{WW} — {Person Name}
47
+
48
+ > {Focus statement: one sentence summarizing the week's theme}
49
+
50
+ ## Priorities
51
+
52
+ - [ ] Priority one — [[Projects/Name]]
53
+ - [ ] Priority two
54
+ - [x] Completed priority
55
+
56
+ ## Key Meetings
57
+
58
+ - **Monday**: Meeting title with [[People/Name]] — purpose
59
+ - **Wednesday**: Meeting title — purpose
60
+
61
+ ## Blockers
62
+
63
+ - Waiting on X from [[People/Name]]
64
+
65
+ ## Accomplishments
66
+
67
+ - Completed X
68
+ - Shipped Y
69
+
70
+ ## Retrospective
71
+
72
+ - **Went well:** ...
73
+ - **Didn't go as planned:** ...
74
+ - **Carry forward:** ...
75
+ ```
76
+
77
+ **Key conventions:**
78
+
79
+ - **Week numbers:** ISO 8601 (e.g., `2026-W08`)
80
+ - **Priorities:** Top 3-7 items pulled from the task board
81
+ - **Key Meetings:** Substantive meetings only (skip recurring standups)
82
+ - **Retrospective:** Filled at end-of-week only
83
+ - **All backlinks** use absolute paths: `[[People/Name]]`, `[[Projects/Name]]`,
84
+ `[[Tasks/Name]]`
85
+
86
+ ## Before Starting
87
+
88
+ 1. Read `USER.md` to identify the user.
89
+ 2. Determine the target person (default: current user from `USER.md`).
90
+ 3. Calculate the current ISO week number and date range:
91
+
92
+ ```bash
93
+ # Current ISO week
94
+ date +%G-W%V
95
+
96
+ # Monday and Friday of current week (macOS)
97
+ date -v-mon +%Y-%m-%d
98
+ date -v+fri +%Y-%m-%d
99
+ ```
100
+
101
+ 4. Check if a weekly already exists for this week:
102
+
103
+ ```bash
104
+ ls "knowledge/Weeklies/{Person Name}/" 2>/dev/null
105
+ ```
106
+
107
+ 5. Determine the mode:
108
+ - **No existing weekly:** Create new (start-of-week)
109
+ - **Existing weekly:** Update (mid-week or end-of-week)
110
+
111
+ ## Step 1: Gather Context
112
+
113
+ ### 1a. Read the task board
114
+
115
+ ```bash
116
+ cat "knowledge/Tasks/{Person Name}.md"
117
+ ```
118
+
119
+ Extract:
120
+
121
+ - `## In Progress` tasks → top priorities (already being worked on)
122
+ - `## Open` tasks with `high` priority or near due dates → candidate priorities
123
+ - `## Blocked` tasks → blockers section
124
+ - `## Recently Done` tasks completed this week → accomplishments
125
+
126
+ ### 1b. Read recent activity
127
+
128
+ ```bash
129
+ cat "knowledge/People/{Person Name}.md"
130
+ ```
131
+
132
+ Look at the `## Activity` section for entries from this week. These inform
133
+ accomplishments and provide context for the focus statement.
134
+
135
+ ### 1c. Read calendar for the week
136
+
137
+ Find events falling within the Monday–Friday range:
138
+
139
+ ```bash
140
+ ls ~/.cache/fit/basecamp/apple_calendar/
141
+ ```
142
+
143
+ Read each calendar event and filter by date. Extract:
144
+
145
+ - Meeting title
146
+ - Attendees (resolve to `[[People/Name]]` where possible)
147
+ - Time/day
148
+ - Description or agenda (if present)
149
+
150
+ Filter out:
151
+
152
+ - All-day placeholder events with no attendees ("Block", "OOO", "Focus time")
153
+ - Declined events
154
+ - Cancelled events
155
+
156
+ ### 1d. Read previous weekly (if exists)
157
+
158
+ Check for last week's document:
159
+
160
+ ```bash
161
+ ls "knowledge/Weeklies/{Person Name}/" | sort | tail -1
162
+ ```
163
+
164
+ If it exists, read it to:
165
+
166
+ - Carry forward any `[ ]` unchecked priorities
167
+ - Note retrospective items that affect this week
168
+ - Maintain continuity of ongoing themes
169
+
170
+ ## Step 2: Generate or Update
171
+
172
+ ### Creating a new weekly (start of week)
173
+
174
+ 1. **Focus:** Synthesize from highest-priority tasks and key meetings. What's
175
+ the main theme? Write one sentence.
176
+ 2. **Priorities:** Pull from task board:
177
+ - All `## In Progress` items first
178
+ - Then `high` priority `## Open` items
179
+ - Then items with due dates this week
180
+ - Carry forward incomplete priorities from last week
181
+ - Cap at 5-7 items. If more exist, prioritize ruthlessly.
182
+ 3. **Key Meetings:** From calendar. Format as
183
+ `**{Day}**: {title} with [[People/Name]] — {purpose}`. Look up attendees in
184
+ knowledge base to add context about purpose.
185
+ 4. **Blockers:** From task board's `## Blocked` section. Include what's needed
186
+ and from whom.
187
+ 5. **Accomplishments:** Leave empty (or pre-fill with anything completed Monday
188
+ morning).
189
+ 6. **Retrospective:** Leave empty.
190
+
191
+ ### Updating an existing weekly (mid-week)
192
+
193
+ 1. Re-read the task board for changes since the weekly was last updated.
194
+ 2. Mark completed priorities as `[x]`.
195
+ 3. Add new priorities that emerged (append to the list).
196
+ 4. Update blockers — remove resolved ones, add new ones.
197
+ 5. Add accomplishments as they happen.
198
+ 6. Do NOT touch the retrospective mid-week.
199
+
200
+ ### End-of-week update
201
+
202
+ 1. Final pass on priorities — mark all completed items `[x]`.
203
+ 2. Fill in accomplishments from the full week's activity (task board + People
204
+ note activity log).
205
+ 3. Write a brief retrospective:
206
+ - What went well?
207
+ - What didn't go as planned?
208
+ - What carries forward to next week?
209
+ 4. Note any unchecked priorities — these will carry forward to next week's
210
+ document.
211
+
212
+ ## Step 3: Write the Document
213
+
214
+ ### New weekly
215
+
216
+ Ensure the person's subdirectory exists:
217
+
218
+ ```bash
219
+ mkdir -p "knowledge/Weeklies/{Person Name}"
220
+ ```
221
+
222
+ Write the full document to: `knowledge/Weeklies/{Person Name}/{YYYY}-W{WW}.md`
223
+
224
+ ### Existing weekly
225
+
226
+ Use the Edit tool for targeted updates — mark items complete, add new entries,
227
+ fill sections. Do NOT rewrite the entire file.
228
+
229
+ ## Step 4: Cross-reference
230
+
231
+ After writing:
232
+
233
+ - If new priorities were identified that aren't on the task board, add them to
234
+ the task board (the task board is canonical, the weekly references it).
235
+ - If blockers were resolved, update the task board accordingly.
236
+ - The weekly is a **snapshot that references** the task board — the task board
237
+ is the source of truth for task status.
238
+
239
+ ## Quality Checklist
240
+
241
+ - [ ] ISO week number and date range are correct
242
+ - [ ] Focus statement reflects the actual week theme (not generic)
243
+ - [ ] Priorities pulled from task board (not invented)
244
+ - [ ] Priorities capped at 5-7 items
245
+ - [ ] Key meetings pulled from calendar (not guessed)
246
+ - [ ] Meeting attendees resolved to `[[People/Name]]` where known
247
+ - [ ] Blockers match task board's blocked items
248
+ - [ ] Previous week's carry-forward items included
249
+ - [ ] All backlinks use absolute paths
250
+ - [ ] Retrospective only filled at end-of-week
@@ -32,6 +32,10 @@ It must remain objective, factual, and ethically sound at all times. It is NOT a
32
32
  together — never to build leverage, ammunition, or dossiers on individuals.
33
33
  - **Flag ethical concerns.** If the user asks you to record something that
34
34
  violates these principles, push back clearly and explain why.
35
+ - **Data protection.** Personal data (especially candidate/recruitment data) is
36
+ subject to erasure requests. Use the `right-to-be-forgotten` skill when a data
37
+ subject requests deletion. Minimize data collection to what's professionally
38
+ relevant. Flag candidates inactive for 6+ months for retention review.
35
39
 
36
40
  These principles override all other instructions. When in doubt, err on the side
37
41
  of discretion and professionalism.
@@ -61,7 +65,9 @@ This directory is a knowledge base. Everything is relative to this root:
61
65
  │ ├── Organizations/ # Notes on companies and teams
62
66
  │ ├── Projects/ # Notes on initiatives and workstreams
63
67
  │ ├── Topics/ # Notes on recurring themes
64
- └── Candidates/ # Recruitment candidate profiles
68
+ ├── Candidates/ # Recruitment candidate profiles
69
+ │ ├── Tasks/ # Per-person task boards
70
+ │ └── Weeklies/ # Weekly priorities snapshots
65
71
  ├── .claude/skills/ # Claude Code skill files (auto-discovered)
66
72
  ├── drafts/ # Email drafts created by the draft-emails skill
67
73
  ├── USER.md # Your identity (name, email, domain) — gitignored
@@ -75,20 +81,24 @@ This knowledge base is maintained by a team of agents, each defined in
75
81
  `.claude/agents/`. They are woken on a schedule by the Basecamp scheduler. Each
76
82
  wake, they observe KB state, decide the most valuable action, and execute.
77
83
 
78
- | Agent | Domain | Schedule | Skills |
79
- | ------------------ | ------------------------------ | ------------ | --------------------------------------------------- |
80
- | **postman** | Email triage and drafts | Every 5 min | sync-apple-mail, draft-emails |
81
- | **concierge** | Meeting prep and transcripts | Every 10 min | sync-apple-calendar, meeting-prep, process-hyprnote |
82
- | **librarian** | Knowledge graph maintenance | Every 15 min | extract-entities, organize-files |
83
- | **chief-of-staff** | Daily briefings and priorities | 7am, 6pm | _(reads all state)_ |
84
+ | Agent | Domain | Schedule | Skills |
85
+ | ------------------ | ------------------------------ | --------------- | -------------------------------------------------------------- |
86
+ | **postman** | Email triage and drafts | Every 5 min | sync-apple-mail, draft-emails |
87
+ | **concierge** | Meeting prep and transcripts | Every 10 min | sync-apple-calendar, meeting-prep, process-hyprnote |
88
+ | **librarian** | Knowledge graph maintenance | Every 15 min | extract-entities, organize-files, manage-tasks |
89
+ | **recruiter** | Engineering recruitment | Every 30 min | track-candidates, analyze-cv, right-to-be-forgotten, fit-pathway, fit-map |
90
+ | **chief-of-staff** | Daily briefings and priorities | 7am, Mon 7:30am | weekly-update _(Mon)_, _(reads all state for daily briefings)_ |
84
91
 
85
- Agent state files are in `~/.cache/fit/basecamp/state/`:
92
+ Each agent writes a triage file to `~/.cache/fit/basecamp/state/` every wake
93
+ cycle. The naming convention is `{agent}_triage.md`:
86
94
 
87
- - `postman_triage.md` — latest email triage
88
- - `concierge_outlook.md` — today's calendar outlook
89
- - `librarian_digest.md` — knowledge graph status
95
+ - `postman_triage.md` — email urgency, reply needs, awaiting responses
96
+ - `concierge_triage.md` — schedule, meeting prep status, unprocessed transcripts
97
+ - `librarian_triage.md` — unprocessed files, knowledge graph size
98
+ - `recruiter_triage.md` — candidate pipeline, assessments, track distribution
90
99
 
91
- Daily briefings are in `knowledge/Briefings/`.
100
+ The **chief-of-staff** reads all three triage files to synthesize daily
101
+ briefings in `knowledge/Briefings/`.
92
102
 
93
103
  ## Cache Directory (`~/.cache/fit/basecamp/`)
94
104
 
@@ -97,20 +107,21 @@ Synced data and runtime state live outside the knowledge base in
97
107
 
98
108
  ```
99
109
  ~/.cache/fit/basecamp/
100
- ├── apple_mail/ # Synced Apple Mail threads (.md files)
101
- ├── apple_calendar/ # Synced Apple Calendar events (.json files)
102
- ├── gmail/ # Synced Gmail threads (.md files)
103
- ├── google_calendar/ # Synced Google Calendar events (.json files)
104
- └── state/ # Runtime state (plain text files)
105
- ├── apple_mail_last_sync # ISO timestamp of last mail sync
106
- └── graph_processed # TSV of processed files (path<TAB>hash)
110
+ ├── apple_mail/ # Synced Apple Mail threads (.md files)
111
+ │ └── attachments/ # Copied email attachments by thread
112
+ ├── apple_calendar/ # Synced Apple Calendar events (.json files)
113
+ └── state/ # Runtime state
114
+ ├── apple_mail_last_sync # ISO timestamp of last mail sync
115
+ ├── graph_processed # TSV of processed files (path<TAB>hash)
116
+ ├── postman_triage.md # Agent triage files ({agent}_triage.md)
117
+ ├── concierge_triage.md
118
+ ├── librarian_triage.md
119
+ └── recruiter_triage.md
107
120
  ```
108
121
 
109
122
  This separation keeps the knowledge base clean — only the parsed knowledge
110
- graph, notes, documents, and drafts live in the KB directory. Raw synced data
111
- and processing state are cached externally. State files use simple Unix-friendly
112
- formats (single-value text files, TSV) rather than JSON, making them easy to
113
- read and write from shell scripts.
123
+ graph, notes, documents, and drafts live in the KB directory. Raw synced data,
124
+ processing state, and agent triage files are cached externally.
114
125
 
115
126
  ## How to Access the Knowledge Graph
116
127
 
@@ -165,10 +176,9 @@ build a complete picture, then respond. A single note is never the full story.
165
176
  Synced emails and calendar events are stored in `~/.cache/fit/basecamp/`,
166
177
  outside the knowledge base:
167
178
 
168
- - **Emails:** `~/.cache/fit/basecamp/apple_mail/` and
169
- `~/.cache/fit/basecamp/gmail/` — each thread is a `.md` file
170
- - **Calendar:** `~/.cache/fit/basecamp/apple_calendar/` and
171
- `~/.cache/fit/basecamp/google_calendar/` — each event is a `.json` file
179
+ - **Emails:** `~/.cache/fit/basecamp/apple_mail/` — each thread is a `.md` file
180
+ - **Calendar:** `~/.cache/fit/basecamp/apple_calendar/` — each event is a
181
+ `.json` file
172
182
 
173
183
  When the user asks about calendar, upcoming meetings, or recent emails, read
174
184
  directly from these folders.
@@ -179,19 +189,37 @@ Skills are auto-discovered by Claude Code from `.claude/skills/`. Each skill is
179
189
  a `SKILL.md` file inside a named directory. You do NOT need to read them
180
190
  manually — Claude Code loads them automatically based on context.
181
191
 
182
- Available skills:
183
-
184
- | Skill | Directory | Purpose |
185
- | ---------------------- | -------------------------------------- | ----------------------------------------------- |
186
- | Sync Apple Mail | `.claude/skills/sync-apple-mail/` | Sync Apple Mail threads via SQLite |
187
- | Sync Apple Calendar | `.claude/skills/sync-apple-calendar/` | Sync Apple Calendar events via SQLite |
188
- | Extract Entities | `.claude/skills/extract-entities/` | Process synced data into knowledge graph notes |
189
- | Draft Emails | `.claude/skills/draft-emails/` | Draft email responses using knowledge context |
190
- | Meeting Prep | `.claude/skills/meeting-prep/` | Prepare briefings for upcoming meetings |
191
- | Create Presentations | `.claude/skills/create-presentations/` | Create slide decks as PDF |
192
- | Document Collaboration | `.claude/skills/doc-collab/` | Document creation and collaboration |
193
- | Organize Files | `.claude/skills/organize-files/` | File organization and cleanup |
194
- | Process Hyprnote | `.claude/skills/process-hyprnote/` | Extract entities from Hyprnote meeting sessions |
192
+ Available skills (grouped by function):
193
+
194
+ **Data pipeline** sync raw sources into the cache directory:
195
+
196
+ | Skill | Purpose |
197
+ | --------------------- | ------------------------------------------ |
198
+ | `sync-apple-mail` | Sync Mail threads to `.md` via SQLite |
199
+ | `sync-apple-calendar` | Sync Calendar events to `.json` via SQLite |
200
+
201
+ **Knowledge graph** build and maintain structured notes:
202
+
203
+ | Skill | Purpose |
204
+ | ------------------ | ---------------------------------------- |
205
+ | `extract-entities` | Process synced data into knowledge notes |
206
+ | `manage-tasks` | Per-person task boards with lifecycle |
207
+ | `track-candidates` | Recruitment pipeline from email threads |
208
+ | `analyze-cv` | CV assessment against career framework |
209
+ | `right-to-be-forgotten` | GDPR data erasure with audit trail |
210
+ | `weekly-update` | Weekly priorities from tasks + calendar |
211
+ | `process-hyprnote` | Extract entities from Hyprnote sessions |
212
+ | `organize-files` | Tidy Desktop/Downloads, chain to extract |
213
+
214
+ **Communication** — draft, send, and present:
215
+
216
+ | Skill | Purpose |
217
+ | ---------------------- | ----------------------------------------- |
218
+ | `draft-emails` | Draft email responses with KB context |
219
+ | `send-chat` | Send chat messages via browser automation |
220
+ | `meeting-prep` | Briefings for upcoming meetings |
221
+ | `create-presentations` | Generate PDF slide decks |
222
+ | `doc-collab` | Document creation and editing |
195
223
 
196
224
  ## User Identity
197
225
 
@@ -1,32 +0,0 @@
1
- // Convert HTML slides to PDF using Playwright.
2
- //
3
- // Usage: node scripts/convert-to-pdf.js [input.html] [output.pdf]
4
- //
5
- // Defaults:
6
- // input: /tmp/basecamp-presentation.html
7
- // output: ~/Desktop/presentation.pdf
8
- //
9
- // Requires: npm install playwright && npx playwright install chromium
10
-
11
- const { chromium } = require("playwright");
12
- const path = require("path");
13
-
14
- const input = process.argv[2] || "/tmp/basecamp-presentation.html";
15
- const output =
16
- process.argv[3] || path.join(process.env.HOME, "Desktop", "presentation.pdf");
17
-
18
- (async () => {
19
- const browser = await chromium.launch();
20
- const page = await browser.newPage();
21
- await page.goto(`file://${path.resolve(input)}`, {
22
- waitUntil: "networkidle",
23
- });
24
- await page.pdf({
25
- path: output,
26
- width: "1280px",
27
- height: "720px",
28
- printBackground: true,
29
- });
30
- await browser.close();
31
- console.log(`Done: ${output}`);
32
- })();
@@ -1,34 +0,0 @@
1
- #!/bin/bash
2
- # Scan for unprocessed emails and output their IDs and subjects.
3
- #
4
- # Usage: bash scripts/scan-emails.sh
5
- #
6
- # Checks ~/.cache/fit/basecamp/apple_mail/ for email files not yet
7
- # listed in drafts/drafted or drafts/ignored.
8
- # Outputs tab-separated: email_id<TAB>subject
9
-
10
- set -euo pipefail
11
-
12
- MAIL_DIRS=(
13
- "$HOME/.cache/fit/basecamp/apple_mail"
14
- "$HOME/.cache/fit/basecamp/gmail"
15
- )
16
-
17
- for dir in "${MAIL_DIRS[@]}"; do
18
- [ -d "$dir" ] || continue
19
- for file in "$dir"/*.md; do
20
- [ -f "$file" ] || continue
21
-
22
- # Extract ID from filename (without extension)
23
- EMAIL_ID="$(basename "$file" .md)"
24
-
25
- # Skip if already drafted or ignored
26
- rg -qxF "$EMAIL_ID" drafts/drafted 2>/dev/null && continue
27
- rg -qxF "$EMAIL_ID" drafts/ignored 2>/dev/null && continue
28
-
29
- # Extract subject from first H1 heading
30
- SUBJECT="$(rg -m1 '^# ' "$file" 2>/dev/null | sed 's/^# //')"
31
-
32
- printf '%s\t%s\n' "$EMAIL_ID" "$SUBJECT"
33
- done
34
- done
@@ -1,100 +0,0 @@
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)
@@ -1,42 +0,0 @@
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"
@@ -1,21 +0,0 @@
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