@forwardimpact/basecamp 0.3.0 → 2.0.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.
- package/README.md +70 -85
- package/config/scheduler.json +13 -17
- package/package.json +11 -13
- package/src/basecamp.js +742 -0
- package/template/.claude/agents/chief-of-staff.md +99 -0
- package/template/.claude/agents/concierge.md +76 -0
- package/template/.claude/agents/librarian.md +61 -0
- package/template/.claude/agents/postman.md +73 -0
- package/template/.claude/settings.json +49 -0
- package/template/.claude/skills/draft-emails/SKILL.md +32 -3
- package/template/.claude/skills/draft-emails/scripts/scan-emails.sh +3 -2
- package/template/.claude/skills/extract-entities/SKILL.md +0 -1
- package/template/.claude/skills/extract-entities/references/TEMPLATES.md +1 -0
- package/template/.claude/skills/process-hyprnote/SKILL.md +335 -0
- package/template/.claude/skills/sync-apple-calendar/SKILL.md +6 -3
- package/template/.claude/skills/sync-apple-calendar/scripts/sync.py +13 -4
- package/template/.claude/skills/sync-apple-mail/SKILL.md +17 -5
- package/template/.claude/skills/sync-apple-mail/references/SCHEMA.md +32 -5
- package/template/.claude/skills/sync-apple-mail/scripts/sync.py +134 -27
- package/template/CLAUDE.md +81 -14
- package/template/knowledge/Briefings/.gitkeep +0 -0
- package/basecamp.js +0 -660
- package/build.js +0 -122
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
Queries the macOS Mail SQLite database for threads with new messages since
|
|
5
5
|
the last sync and writes one markdown file per thread.
|
|
6
6
|
|
|
7
|
-
Usage: python3 scripts/sync.py
|
|
7
|
+
Usage: python3 scripts/sync.py [--days N]
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--days N How many days back to sync on first run (default: 30)
|
|
8
11
|
|
|
9
12
|
Requires: macOS with Mail app configured and Full Disk Access granted.
|
|
10
13
|
"""
|
|
11
14
|
|
|
15
|
+
import argparse
|
|
12
16
|
import importlib.util
|
|
13
17
|
import json
|
|
14
18
|
import os
|
|
19
|
+
import shutil
|
|
15
20
|
import subprocess
|
|
16
21
|
import sys
|
|
17
22
|
import time
|
|
@@ -19,6 +24,7 @@ from datetime import datetime, timezone, timedelta
|
|
|
19
24
|
from pathlib import Path
|
|
20
25
|
|
|
21
26
|
OUTDIR = os.path.expanduser("~/.cache/fit/basecamp/apple_mail")
|
|
27
|
+
ATTACHMENTS_DIR = os.path.join(OUTDIR, "attachments")
|
|
22
28
|
STATE_DIR = os.path.expanduser("~/.cache/fit/basecamp/state")
|
|
23
29
|
STATE_FILE = os.path.join(STATE_DIR, "apple_mail_last_sync")
|
|
24
30
|
|
|
@@ -64,8 +70,12 @@ def query(db, sql, retry=True):
|
|
|
64
70
|
return json.loads(result.stdout) if result.stdout.strip() else []
|
|
65
71
|
|
|
66
72
|
|
|
67
|
-
def load_last_sync():
|
|
68
|
-
"""Load the last sync timestamp. Returns Unix timestamp.
|
|
73
|
+
def load_last_sync(days_back=30):
|
|
74
|
+
"""Load the last sync timestamp. Returns Unix timestamp.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
days_back: How many days back to look on first sync (default: 30).
|
|
78
|
+
"""
|
|
69
79
|
try:
|
|
70
80
|
iso = Path(STATE_FILE).read_text().strip()
|
|
71
81
|
if iso:
|
|
@@ -75,8 +85,8 @@ def load_last_sync():
|
|
|
75
85
|
return int(dt.timestamp())
|
|
76
86
|
except (FileNotFoundError, ValueError):
|
|
77
87
|
pass
|
|
78
|
-
# First sync:
|
|
79
|
-
return int((datetime.now(timezone.utc) - timedelta(days=
|
|
88
|
+
# First sync: N days ago
|
|
89
|
+
return int((datetime.now(timezone.utc) - timedelta(days=days_back)).timestamp())
|
|
80
90
|
|
|
81
91
|
|
|
82
92
|
def save_sync_state():
|
|
@@ -176,28 +186,64 @@ def fetch_recipients(db, message_ids):
|
|
|
176
186
|
return result
|
|
177
187
|
|
|
178
188
|
|
|
179
|
-
def
|
|
180
|
-
"""
|
|
189
|
+
def fetch_attachments(db, message_ids):
|
|
190
|
+
"""Batch-fetch attachment metadata for a set of message IDs."""
|
|
191
|
+
if not message_ids:
|
|
192
|
+
return {}
|
|
193
|
+
id_list = ",".join(str(mid) for mid in message_ids)
|
|
194
|
+
rows = query(db, f"""
|
|
195
|
+
SELECT a.message AS message_id, a.attachment_id, a.name
|
|
196
|
+
FROM attachments a
|
|
197
|
+
WHERE a.message IN ({id_list})
|
|
198
|
+
ORDER BY a.message, a.ROWID;
|
|
199
|
+
""")
|
|
200
|
+
result = {}
|
|
201
|
+
for r in rows:
|
|
202
|
+
mid = r["message_id"]
|
|
203
|
+
result.setdefault(mid, []).append({
|
|
204
|
+
"attachment_id": r["attachment_id"],
|
|
205
|
+
"name": r["name"],
|
|
206
|
+
})
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def build_file_indexes():
|
|
211
|
+
"""Build emlx and attachment indexes with a single find traversal.
|
|
212
|
+
|
|
213
|
+
Returns (emlx_index, attachment_index):
|
|
214
|
+
emlx_index: {message_rowid: path} for .emlx files
|
|
215
|
+
attachment_index: {(message_rowid, attachment_id): path} for attachment files
|
|
216
|
+
"""
|
|
181
217
|
mail_dir = os.path.expanduser("~/Library/Mail")
|
|
182
218
|
try:
|
|
183
219
|
result = subprocess.run(
|
|
184
|
-
["find", mail_dir, "-name", "*.emlx"
|
|
185
|
-
|
|
220
|
+
["find", mail_dir, "(", "-name", "*.emlx", "-o",
|
|
221
|
+
"-path", "*/Attachments/*", ")", "-type", "f"],
|
|
222
|
+
capture_output=True, text=True, timeout=60
|
|
186
223
|
)
|
|
187
|
-
|
|
224
|
+
emlx_index = {}
|
|
225
|
+
attachment_index = {}
|
|
188
226
|
for path in result.stdout.strip().splitlines():
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
227
|
+
if "/Attachments/" in path:
|
|
228
|
+
# Path: .../Attachments/{msg_rowid}/{attachment_id}/{filename}
|
|
229
|
+
parts = path.split("/Attachments/", 1)
|
|
230
|
+
if len(parts) == 2:
|
|
231
|
+
segments = parts[1].split("/")
|
|
232
|
+
if len(segments) >= 3 and segments[0].isdigit():
|
|
233
|
+
msg_rowid = int(segments[0])
|
|
234
|
+
att_id = segments[1]
|
|
235
|
+
attachment_index[(msg_rowid, att_id)] = path
|
|
236
|
+
elif path.endswith(".emlx"):
|
|
237
|
+
basename = os.path.basename(path)
|
|
238
|
+
msg_id = basename.split(".")[0]
|
|
239
|
+
if msg_id.isdigit():
|
|
240
|
+
mid = int(msg_id)
|
|
241
|
+
# Prefer .emlx over .partial.emlx (shorter name = full message)
|
|
242
|
+
if mid not in emlx_index or len(basename) < len(os.path.basename(emlx_index[mid])):
|
|
243
|
+
emlx_index[mid] = path
|
|
244
|
+
return emlx_index, attachment_index
|
|
199
245
|
except (subprocess.TimeoutExpired, Exception):
|
|
200
|
-
return {}
|
|
246
|
+
return {}, {}
|
|
201
247
|
|
|
202
248
|
|
|
203
249
|
def parse_emlx(message_id, emlx_index):
|
|
@@ -234,7 +280,44 @@ def format_sender(msg):
|
|
|
234
280
|
return addr or name
|
|
235
281
|
|
|
236
282
|
|
|
237
|
-
def
|
|
283
|
+
def copy_thread_attachments(thread_id, messages, attachments_by_msg, attachment_index):
|
|
284
|
+
"""Copy attachment files into the output attachments directory.
|
|
285
|
+
|
|
286
|
+
Returns {message_id: [{name, available, path}, ...]} for markdown listing.
|
|
287
|
+
"""
|
|
288
|
+
results = {}
|
|
289
|
+
seen_filenames = set()
|
|
290
|
+
for msg in messages:
|
|
291
|
+
mid = msg["message_id"]
|
|
292
|
+
msg_attachments = attachments_by_msg.get(mid, [])
|
|
293
|
+
if not msg_attachments:
|
|
294
|
+
continue
|
|
295
|
+
msg_results = []
|
|
296
|
+
for att in msg_attachments:
|
|
297
|
+
att_id = att["attachment_id"]
|
|
298
|
+
name = att["name"] or "unnamed"
|
|
299
|
+
source = attachment_index.get((mid, att_id))
|
|
300
|
+
if not source or not os.path.isfile(source):
|
|
301
|
+
msg_results.append({"name": name, "available": False, "path": None})
|
|
302
|
+
continue
|
|
303
|
+
# Handle filename collisions by prefixing with message_id
|
|
304
|
+
dest_name = name
|
|
305
|
+
if dest_name in seen_filenames:
|
|
306
|
+
dest_name = f"{mid}_{name}"
|
|
307
|
+
seen_filenames.add(dest_name)
|
|
308
|
+
dest_dir = os.path.join(ATTACHMENTS_DIR, str(thread_id))
|
|
309
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
310
|
+
dest_path = os.path.join(dest_dir, dest_name)
|
|
311
|
+
try:
|
|
312
|
+
shutil.copy2(source, dest_path)
|
|
313
|
+
msg_results.append({"name": dest_name, "available": True, "path": dest_path})
|
|
314
|
+
except OSError:
|
|
315
|
+
msg_results.append({"name": name, "available": False, "path": None})
|
|
316
|
+
results[mid] = msg_results
|
|
317
|
+
return results
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def write_thread_markdown(thread_id, messages, recipients_by_msg, emlx_index, attachment_results=None):
|
|
238
321
|
"""Write a thread as a markdown file."""
|
|
239
322
|
if not messages:
|
|
240
323
|
return False
|
|
@@ -286,17 +369,34 @@ def write_thread_markdown(thread_id, messages, recipients_by_msg, emlx_index):
|
|
|
286
369
|
lines.append(body)
|
|
287
370
|
lines.append("")
|
|
288
371
|
|
|
372
|
+
# Attachments
|
|
373
|
+
if attachment_results:
|
|
374
|
+
msg_atts = attachment_results.get(mid, [])
|
|
375
|
+
if msg_atts:
|
|
376
|
+
lines.append("**Attachments:**")
|
|
377
|
+
for att in msg_atts:
|
|
378
|
+
if att["available"]:
|
|
379
|
+
lines.append(f"- [{att['name']}](attachments/{thread_id}/{att['name']})")
|
|
380
|
+
else:
|
|
381
|
+
lines.append(f"- {att['name']} *(not available)*")
|
|
382
|
+
lines.append("")
|
|
383
|
+
|
|
289
384
|
filepath = os.path.join(OUTDIR, f"{thread_id}.md")
|
|
290
385
|
Path(filepath).write_text("\n".join(lines))
|
|
291
386
|
return True
|
|
292
387
|
|
|
293
388
|
|
|
294
389
|
def main():
|
|
390
|
+
parser = argparse.ArgumentParser(description="Sync Apple Mail threads.")
|
|
391
|
+
parser.add_argument("--days", type=int, default=30,
|
|
392
|
+
help="How many days back to sync on first run (default: 30)")
|
|
393
|
+
args = parser.parse_args()
|
|
394
|
+
|
|
295
395
|
db = find_db()
|
|
296
396
|
os.makedirs(OUTDIR, exist_ok=True)
|
|
297
397
|
|
|
298
398
|
# Load sync state
|
|
299
|
-
since_ts = load_last_sync()
|
|
399
|
+
since_ts = load_last_sync(days_back=args.days)
|
|
300
400
|
since_readable = unix_to_readable(since_ts)
|
|
301
401
|
|
|
302
402
|
# Discover thread column
|
|
@@ -317,8 +417,8 @@ def main():
|
|
|
317
417
|
save_sync_state()
|
|
318
418
|
return
|
|
319
419
|
|
|
320
|
-
# Build .emlx file
|
|
321
|
-
emlx_index =
|
|
420
|
+
# Build .emlx and attachment file indexes (single find traversal)
|
|
421
|
+
emlx_index, attachment_index = build_file_indexes()
|
|
322
422
|
|
|
323
423
|
# Process each thread
|
|
324
424
|
written = 0
|
|
@@ -327,11 +427,18 @@ def main():
|
|
|
327
427
|
if not messages:
|
|
328
428
|
continue
|
|
329
429
|
|
|
330
|
-
# Batch-fetch recipients for all messages in thread
|
|
331
430
|
msg_ids = [m["message_id"] for m in messages]
|
|
431
|
+
|
|
432
|
+
# Batch-fetch recipients and attachments for all messages in thread
|
|
332
433
|
recipients = fetch_recipients(db, msg_ids)
|
|
434
|
+
attachments_by_msg = fetch_attachments(db, msg_ids)
|
|
435
|
+
|
|
436
|
+
# Copy attachment files to output directory
|
|
437
|
+
attachment_results = copy_thread_attachments(
|
|
438
|
+
tid, messages, attachments_by_msg, attachment_index
|
|
439
|
+
)
|
|
333
440
|
|
|
334
|
-
if write_thread_markdown(tid, messages, recipients, emlx_index):
|
|
441
|
+
if write_thread_markdown(tid, messages, recipients, emlx_index, attachment_results):
|
|
335
442
|
written += 1
|
|
336
443
|
|
|
337
444
|
# Save sync state (even on partial success)
|
package/template/CLAUDE.md
CHANGED
|
@@ -5,6 +5,37 @@ prepping for meetings, tracking projects, and answering questions — backed by
|
|
|
5
5
|
live knowledge graph built from their emails, calendar, and meeting notes.
|
|
6
6
|
Everything lives locally on this machine.
|
|
7
7
|
|
|
8
|
+
## Ethics & Integrity — NON-NEGOTIABLE
|
|
9
|
+
|
|
10
|
+
This knowledge base is a **professional tool shared with trusted team members**.
|
|
11
|
+
It must remain objective, factual, and ethically sound at all times. It is NOT a
|
|
12
|
+
"black book" and must NEVER become one.
|
|
13
|
+
|
|
14
|
+
**Hard rules:**
|
|
15
|
+
|
|
16
|
+
- **Objective and factual only.** Every note must reflect verifiable facts —
|
|
17
|
+
what was said, decided, or observed. No speculation, gossip, or
|
|
18
|
+
editorializing.
|
|
19
|
+
- **No personal judgments about character.** Do not record subjective opinions
|
|
20
|
+
about people's personalities, competence, or trustworthiness. Stick to what
|
|
21
|
+
happened: actions, decisions, stated positions.
|
|
22
|
+
- **No sensitive personal information beyond what's work-relevant.** Do not
|
|
23
|
+
store health details, personal relationships, political views, or other
|
|
24
|
+
private matters unless directly relevant to a professional interaction the
|
|
25
|
+
person themselves shared.
|
|
26
|
+
- **Fair and balanced.** If a disagreement or conflict is noted, represent all
|
|
27
|
+
sides accurately. Never frame notes to make someone look bad.
|
|
28
|
+
- **Assume the subject will read it.** Write every note as if the person it's
|
|
29
|
+
about will see it. If you wouldn't be comfortable showing it to them, don't
|
|
30
|
+
write it.
|
|
31
|
+
- **No weaponization.** This knowledge base exists to help the team work better
|
|
32
|
+
together — never to build leverage, ammunition, or dossiers on individuals.
|
|
33
|
+
- **Flag ethical concerns.** If the user asks you to record something that
|
|
34
|
+
violates these principles, push back clearly and explain why.
|
|
35
|
+
|
|
36
|
+
These principles override all other instructions. When in doubt, err on the side
|
|
37
|
+
of discretion and professionalism.
|
|
38
|
+
|
|
8
39
|
## Personality
|
|
9
40
|
|
|
10
41
|
- **Supportive thoroughness:** Explain complex topics clearly and completely.
|
|
@@ -29,7 +60,8 @@ This directory is a knowledge base. Everything is relative to this root:
|
|
|
29
60
|
│ ├── People/ # Notes on individuals
|
|
30
61
|
│ ├── Organizations/ # Notes on companies and teams
|
|
31
62
|
│ ├── Projects/ # Notes on initiatives and workstreams
|
|
32
|
-
│
|
|
63
|
+
│ ├── Topics/ # Notes on recurring themes
|
|
64
|
+
│ └── Candidates/ # Recruitment candidate profiles
|
|
33
65
|
├── .claude/skills/ # Claude Code skill files (auto-discovered)
|
|
34
66
|
├── drafts/ # Email drafts created by the draft-emails skill
|
|
35
67
|
├── USER.md # Your identity (name, email, domain) — gitignored
|
|
@@ -37,6 +69,27 @@ This directory is a knowledge base. Everything is relative to this root:
|
|
|
37
69
|
└── .mcp.json # MCP server configurations (optional)
|
|
38
70
|
```
|
|
39
71
|
|
|
72
|
+
## Agents
|
|
73
|
+
|
|
74
|
+
This knowledge base is maintained by a team of agents, each defined in
|
|
75
|
+
`.claude/agents/`. They are woken on a schedule by the Basecamp scheduler. Each
|
|
76
|
+
wake, they observe KB state, decide the most valuable action, and execute.
|
|
77
|
+
|
|
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
|
+
|
|
85
|
+
Agent state files are in `~/.cache/fit/basecamp/state/`:
|
|
86
|
+
|
|
87
|
+
- `postman_triage.md` — latest email triage
|
|
88
|
+
- `concierge_outlook.md` — today's calendar outlook
|
|
89
|
+
- `librarian_digest.md` — knowledge graph status
|
|
90
|
+
|
|
91
|
+
Daily briefings are in `knowledge/Briefings/`.
|
|
92
|
+
|
|
40
93
|
## Cache Directory (`~/.cache/fit/basecamp/`)
|
|
41
94
|
|
|
42
95
|
Synced data and runtime state live outside the knowledge base in
|
|
@@ -46,6 +99,7 @@ Synced data and runtime state live outside the knowledge base in
|
|
|
46
99
|
~/.cache/fit/basecamp/
|
|
47
100
|
├── apple_mail/ # Synced Apple Mail threads (.md files)
|
|
48
101
|
├── apple_calendar/ # Synced Apple Calendar events (.json files)
|
|
102
|
+
├── gmail/ # Synced Gmail threads (.md files)
|
|
49
103
|
├── google_calendar/ # Synced Google Calendar events (.json files)
|
|
50
104
|
└── state/ # Runtime state (plain text files)
|
|
51
105
|
├── apple_mail_last_sync # ISO timestamp of last mail sync
|
|
@@ -87,6 +141,17 @@ by name, you MUST look them up in the knowledge base FIRST before responding. Do
|
|
|
87
141
|
not provide generic responses. Look up the context, then respond with that
|
|
88
142
|
knowledge.
|
|
89
143
|
|
|
144
|
+
**STOP — ALWAYS SEARCH BROADLY FIRST.** Never build a response from a single
|
|
145
|
+
note. Before answering, run a keyword search across the entire knowledge graph:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
rg "keyword" knowledge/
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
This surfaces every note that mentions the keyword — people, orgs, projects, and
|
|
152
|
+
topics you might miss if you only open one file. Read ALL matching notes to
|
|
153
|
+
build a complete picture, then respond. A single note is never the full story.
|
|
154
|
+
|
|
90
155
|
**When to access:**
|
|
91
156
|
|
|
92
157
|
- Always when the user mentions a named entity (person, org, project, topic)
|
|
@@ -100,9 +165,10 @@ knowledge.
|
|
|
100
165
|
Synced emails and calendar events are stored in `~/.cache/fit/basecamp/`,
|
|
101
166
|
outside the knowledge base:
|
|
102
167
|
|
|
103
|
-
- **Emails:** `~/.cache/fit/basecamp/apple_mail/`
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
106
172
|
|
|
107
173
|
When the user asks about calendar, upcoming meetings, or recent emails, read
|
|
108
174
|
directly from these folders.
|
|
@@ -115,16 +181,17 @@ manually — Claude Code loads them automatically based on context.
|
|
|
115
181
|
|
|
116
182
|
Available skills:
|
|
117
183
|
|
|
118
|
-
| Skill | Directory | Purpose
|
|
119
|
-
| ---------------------- | -------------------------------------- |
|
|
120
|
-
| Sync Apple Mail | `.claude/skills/sync-apple-mail/` | Sync Apple Mail threads via SQLite
|
|
121
|
-
| Sync Apple Calendar | `.claude/skills/sync-apple-calendar/` | Sync Apple Calendar events via SQLite
|
|
122
|
-
| Extract Entities | `.claude/skills/extract-entities/` | Process synced data into knowledge graph notes
|
|
123
|
-
| Draft Emails | `.claude/skills/draft-emails/` | Draft email responses using knowledge context
|
|
124
|
-
| Meeting Prep | `.claude/skills/meeting-prep/` | Prepare briefings for upcoming meetings
|
|
125
|
-
| Create Presentations | `.claude/skills/create-presentations/` | Create slide decks as PDF
|
|
126
|
-
| Document Collaboration | `.claude/skills/doc-collab/` | Document creation and collaboration
|
|
127
|
-
| Organize Files | `.claude/skills/organize-files/` | File organization and cleanup
|
|
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 |
|
|
128
195
|
|
|
129
196
|
## User Identity
|
|
130
197
|
|
|
File without changes
|