@forwardimpact/basecamp 2.0.0 → 2.2.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/config/scheduler.json +5 -0
- package/package.json +1 -1
- package/src/basecamp.js +288 -57
- package/template/.claude/agents/chief-of-staff.md +6 -2
- package/template/.claude/agents/concierge.md +2 -3
- package/template/.claude/agents/librarian.md +4 -6
- package/template/.claude/agents/recruiter.md +222 -0
- package/template/.claude/settings.json +0 -4
- package/template/.claude/skills/analyze-cv/SKILL.md +267 -0
- package/template/.claude/skills/create-presentations/SKILL.md +2 -2
- package/template/.claude/skills/create-presentations/references/slide.css +1 -1
- package/template/.claude/skills/create-presentations/scripts/convert-to-pdf.mjs +47 -0
- package/template/.claude/skills/draft-emails/SKILL.md +85 -123
- package/template/.claude/skills/draft-emails/scripts/scan-emails.mjs +66 -0
- package/template/.claude/skills/draft-emails/scripts/send-email.mjs +118 -0
- package/template/.claude/skills/extract-entities/SKILL.md +2 -2
- package/template/.claude/skills/extract-entities/scripts/state.mjs +130 -0
- package/template/.claude/skills/manage-tasks/SKILL.md +242 -0
- package/template/.claude/skills/organize-files/SKILL.md +3 -3
- package/template/.claude/skills/organize-files/scripts/organize-by-type.mjs +105 -0
- package/template/.claude/skills/organize-files/scripts/summarize.mjs +84 -0
- package/template/.claude/skills/process-hyprnote/SKILL.md +2 -2
- package/template/.claude/skills/send-chat/SKILL.md +170 -0
- package/template/.claude/skills/sync-apple-calendar/SKILL.md +5 -5
- package/template/.claude/skills/sync-apple-calendar/scripts/sync.mjs +325 -0
- package/template/.claude/skills/sync-apple-mail/SKILL.md +6 -6
- package/template/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +374 -0
- package/template/.claude/skills/sync-apple-mail/scripts/sync.mjs +629 -0
- package/template/.claude/skills/track-candidates/SKILL.md +375 -0
- package/template/.claude/skills/weekly-update/SKILL.md +250 -0
- package/template/CLAUDE.md +63 -40
- package/template/.claude/skills/create-presentations/scripts/convert-to-pdf.js +0 -32
- package/template/.claude/skills/draft-emails/scripts/scan-emails.sh +0 -34
- package/template/.claude/skills/extract-entities/scripts/state.py +0 -100
- package/template/.claude/skills/organize-files/scripts/organize-by-type.sh +0 -42
- package/template/.claude/skills/organize-files/scripts/summarize.sh +0 -21
- package/template/.claude/skills/sync-apple-calendar/scripts/sync.py +0 -242
- package/template/.claude/skills/sync-apple-mail/scripts/parse-emlx.py +0 -104
- package/template/.claude/skills/sync-apple-mail/scripts/sync.py +0 -455
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: manage-tasks
|
|
3
|
+
description: Create, update, list, and close tasks on per-person task boards in knowledge/Tasks/. Manages task lifecycle, extracts action items from other skills, and keeps boards current. Use when the user asks to add, update, or review tasks, or when chained from extract-entities or process-hyprnote.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Manage Tasks
|
|
7
|
+
|
|
8
|
+
Manage per-person task boards in `knowledge/Tasks/`. Each person has a single
|
|
9
|
+
living document that tracks all their open, in-progress, blocked, and recently
|
|
10
|
+
completed tasks. Task boards are the **canonical source** for task tracking —
|
|
11
|
+
other notes (People, Projects) link to them rather than duplicating.
|
|
12
|
+
|
|
13
|
+
## Trigger
|
|
14
|
+
|
|
15
|
+
Run this skill:
|
|
16
|
+
|
|
17
|
+
- When the user asks to add, update, close, or list tasks
|
|
18
|
+
- When chained from `extract-entities` or `process-hyprnote` with extracted
|
|
19
|
+
action items
|
|
20
|
+
- When the user asks to see someone's task board or workload
|
|
21
|
+
- On a schedule to perform housekeeping (prune done items, flag overdue)
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- `knowledge/Tasks/` directory exists
|
|
26
|
+
- User identity configured in `USER.md`
|
|
27
|
+
|
|
28
|
+
## Inputs
|
|
29
|
+
|
|
30
|
+
### User-initiated
|
|
31
|
+
|
|
32
|
+
- Person name and task details from the user's request
|
|
33
|
+
|
|
34
|
+
### Chained from other skills
|
|
35
|
+
|
|
36
|
+
Action items extracted by `extract-entities` or `process-hyprnote`, passed as
|
|
37
|
+
structured data:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
TASKS:
|
|
41
|
+
- Owner: {Full Name}
|
|
42
|
+
Task: {Description — starts with a verb}
|
|
43
|
+
Priority: high|medium|low
|
|
44
|
+
Due: YYYY-MM-DD (if known)
|
|
45
|
+
Source: meeting|email
|
|
46
|
+
Source date: YYYY-MM-DD
|
|
47
|
+
Project: {Project Name} (if applicable)
|
|
48
|
+
Context: {Brief context about where this came from}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Outputs
|
|
52
|
+
|
|
53
|
+
- `knowledge/Tasks/{Person Name}.md` — created or updated task boards
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Task Board Format
|
|
58
|
+
|
|
59
|
+
Each task board is a markdown file with four sections, always in this order:
|
|
60
|
+
|
|
61
|
+
```markdown
|
|
62
|
+
# {Person Name}
|
|
63
|
+
|
|
64
|
+
## In Progress
|
|
65
|
+
## Open
|
|
66
|
+
## Blocked
|
|
67
|
+
## Recently Done
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
All four sections are always present, even if empty.
|
|
71
|
+
|
|
72
|
+
**Task entry format:**
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
- [ ] **{Task title}** | {priority} | due {YYYY-MM-DD} | [[Projects/Name]]
|
|
76
|
+
{Context line with source info and backlinks.}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Key conventions:**
|
|
80
|
+
|
|
81
|
+
- **Priorities:** `high` | `medium` | `low` — omit if medium (default)
|
|
82
|
+
- **Due dates:** Only include if there's a real deadline. Format:
|
|
83
|
+
`due YYYY-MM-DD`
|
|
84
|
+
- **No task IDs.** Tasks are identified by their bold title. Keep titles unique
|
|
85
|
+
within a person's board.
|
|
86
|
+
- **Recently Done:** Keep the last 14 days. Older items pruned during
|
|
87
|
+
housekeeping.
|
|
88
|
+
|
|
89
|
+
## Before Starting
|
|
90
|
+
|
|
91
|
+
1. Read `USER.md` to get user identity.
|
|
92
|
+
2. Determine the operation: **add**, **update**, **close**, **list**, or
|
|
93
|
+
**housekeeping**.
|
|
94
|
+
|
|
95
|
+
## Step 1: Resolve the Person
|
|
96
|
+
|
|
97
|
+
For any task operation, resolve the person to their canonical name:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
ls knowledge/Tasks/
|
|
101
|
+
rg "{name}" knowledge/People/
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If the person doesn't have a task board yet, create one from the template (Step
|
|
105
|
+
4 covers creation).
|
|
106
|
+
|
|
107
|
+
## Step 2: Read Current Task Board
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
cat "knowledge/Tasks/{Person Name}.md"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Parse existing tasks to:
|
|
114
|
+
|
|
115
|
+
- Avoid duplicates (same person, similar description)
|
|
116
|
+
- Understand current workload
|
|
117
|
+
- Find the right insertion point
|
|
118
|
+
|
|
119
|
+
## Step 3: Perform the Operation
|
|
120
|
+
|
|
121
|
+
### Add a Task
|
|
122
|
+
|
|
123
|
+
1. Check for duplicates — same person, similar task title or description. If a
|
|
124
|
+
near-duplicate exists, update it instead of creating a new entry.
|
|
125
|
+
2. Determine the section:
|
|
126
|
+
- Default: `## Open`
|
|
127
|
+
- If user says "I'm working on" / "started" → `## In Progress`
|
|
128
|
+
- If blocked → `## Blocked`
|
|
129
|
+
3. Format the task entry:
|
|
130
|
+
```
|
|
131
|
+
- [ ] **{Task title}** | {priority} | due {YYYY-MM-DD} | [[Projects/Name]]
|
|
132
|
+
{Context line with source info and backlinks.}
|
|
133
|
+
```
|
|
134
|
+
4. Add the entry at the **bottom** of the appropriate section.
|
|
135
|
+
5. If the task references a project, verify the project note exists.
|
|
136
|
+
|
|
137
|
+
### Update a Task
|
|
138
|
+
|
|
139
|
+
1. Find the task by title (fuzzy match OK — bold text between `**`).
|
|
140
|
+
2. Apply changes:
|
|
141
|
+
- **Status change:** Move the entire entry between sections
|
|
142
|
+
- **Priority change:** Update the `| {priority} |` segment
|
|
143
|
+
- **Due date change:** Update or add `| due YYYY-MM-DD |`
|
|
144
|
+
- **Add context:** Append to the indented line
|
|
145
|
+
3. Use the Edit tool for targeted modifications.
|
|
146
|
+
|
|
147
|
+
### Close a Task
|
|
148
|
+
|
|
149
|
+
1. Find the task by title.
|
|
150
|
+
2. Remove it from its current section.
|
|
151
|
+
3. Add to the **top** of `## Recently Done`:
|
|
152
|
+
```
|
|
153
|
+
- [x] **{Task title}** | completed {YYYY-MM-DD}
|
|
154
|
+
```
|
|
155
|
+
(Drop priority, due date, project link, and context — keep it compact.)
|
|
156
|
+
|
|
157
|
+
### List Tasks
|
|
158
|
+
|
|
159
|
+
Query across all task boards:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# All open/in-progress tasks
|
|
163
|
+
rg "^- \[ \] \*\*" knowledge/Tasks/
|
|
164
|
+
|
|
165
|
+
# Tasks for a specific project
|
|
166
|
+
rg "Projects/{Name}" knowledge/Tasks/
|
|
167
|
+
|
|
168
|
+
# High priority tasks
|
|
169
|
+
rg "\| high \|" knowledge/Tasks/
|
|
170
|
+
|
|
171
|
+
# Overdue tasks — find all due dates and compare against today
|
|
172
|
+
rg "due 20[0-9]{2}-[0-9]{2}-[0-9]{2}" knowledge/Tasks/
|
|
173
|
+
|
|
174
|
+
# Blocked tasks
|
|
175
|
+
rg -A1 "^- \[ \]" knowledge/Tasks/ | rg -B1 "Waiting on"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Present results in a clean summary, grouped by person or project as appropriate
|
|
179
|
+
for the user's question.
|
|
180
|
+
|
|
181
|
+
### Housekeeping (Scheduled)
|
|
182
|
+
|
|
183
|
+
Run across all task boards:
|
|
184
|
+
|
|
185
|
+
1. **Prune done items:** Remove completed tasks older than 14 days from
|
|
186
|
+
`## Recently Done`.
|
|
187
|
+
2. **Flag overdue:** Any task with `due {date}` in the past that's still in
|
|
188
|
+
`## Open` or `## In Progress` — check if it needs attention. Do NOT
|
|
189
|
+
auto-modify the task; instead, report overdue items to the user.
|
|
190
|
+
3. **Deduplicate:** If identical tasks appear on the same board, merge them.
|
|
191
|
+
4. **Validate links:** Spot-check that `[[People/]]` and `[[Projects/]]`
|
|
192
|
+
references point to existing notes.
|
|
193
|
+
|
|
194
|
+
## Step 4: Write Updates
|
|
195
|
+
|
|
196
|
+
### New task board
|
|
197
|
+
|
|
198
|
+
Create `knowledge/Tasks/{Person Name}.md` with all four sections:
|
|
199
|
+
|
|
200
|
+
```markdown
|
|
201
|
+
# {Person Name}
|
|
202
|
+
|
|
203
|
+
## In Progress
|
|
204
|
+
|
|
205
|
+
## Open
|
|
206
|
+
|
|
207
|
+
## Blocked
|
|
208
|
+
|
|
209
|
+
## Recently Done
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Existing task board
|
|
213
|
+
|
|
214
|
+
Use the Edit tool to make targeted changes — add, move, or modify individual
|
|
215
|
+
task entries. Do NOT rewrite the entire file.
|
|
216
|
+
|
|
217
|
+
## Step 5: Migrate Open Items (One-time)
|
|
218
|
+
|
|
219
|
+
When first setting up task boards, or when the user asks, migrate existing
|
|
220
|
+
`## Open items` from People and Project notes:
|
|
221
|
+
|
|
222
|
+
1. Scan notes for `## Open items` sections with content:
|
|
223
|
+
```bash
|
|
224
|
+
rg -l "## Open items" knowledge/People/ knowledge/Projects/
|
|
225
|
+
```
|
|
226
|
+
2. For each note with open items, read the items and convert them to task board
|
|
227
|
+
entries.
|
|
228
|
+
3. Add each item to the appropriate person's task board.
|
|
229
|
+
4. **Do NOT remove** the original open items from source notes — they serve as
|
|
230
|
+
the historical record. The task board becomes the living tracker.
|
|
231
|
+
|
|
232
|
+
## Quality Checklist
|
|
233
|
+
|
|
234
|
+
- [ ] Task title is clear and actionable (starts with a verb)
|
|
235
|
+
- [ ] Priority set appropriately (omit if medium)
|
|
236
|
+
- [ ] Due date included only if there's a real deadline
|
|
237
|
+
- [ ] Project linked with `[[Projects/Name]]` if applicable
|
|
238
|
+
- [ ] No duplicate tasks on the board
|
|
239
|
+
- [ ] Recently Done pruned to last 14 days (housekeeping)
|
|
240
|
+
- [ ] All backlinks use absolute paths `[[Folder/Name]]`
|
|
241
|
+
- [ ] Context line is concise (1-2 lines max)
|
|
242
|
+
- [ ] New task board has all four sections present
|
|
@@ -61,7 +61,7 @@ Run when the user asks to find, organize, clean up, or tidy files on their Mac.
|
|
|
61
61
|
|
|
62
62
|
Get an overview of both directories:
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
node scripts/summarize.mjs
|
|
65
65
|
|
|
66
66
|
## Finding Files
|
|
67
67
|
|
|
@@ -79,8 +79,8 @@ find ~/Desktop -maxdepth 1 \( -name "Screenshot*" -o -name "Screen Shot*" \)
|
|
|
79
79
|
Organize a directory into type-based subdirectories (Documents, Images,
|
|
80
80
|
Archives, Installers, Screenshots):
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
node scripts/organize-by-type.mjs ~/Downloads
|
|
83
|
+
node scripts/organize-by-type.mjs ~/Desktop
|
|
84
84
|
|
|
85
85
|
The script creates subdirectories and moves matching files. It does NOT delete
|
|
86
86
|
anything.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Organize files in a directory by type into subdirectories.
|
|
4
|
+
*
|
|
5
|
+
* Scans the top level of the given directory and moves files into category
|
|
6
|
+
* subdirectories: Screenshots, Documents, Images, Archives, and Installers.
|
|
7
|
+
* Categories are determined by file extension and name prefix. Files that do
|
|
8
|
+
* not match any category are left in place. Does NOT delete anything.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
renameSync,
|
|
16
|
+
statSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { extname, join } from "node:path";
|
|
19
|
+
|
|
20
|
+
const HELP = `organize-by-type — sort files into type-based subdirectories
|
|
21
|
+
|
|
22
|
+
Usage: node scripts/organize-by-type.mjs <directory> [-h|--help]
|
|
23
|
+
|
|
24
|
+
Creates subdirectories (Documents, Images, Archives, Installers, Screenshots)
|
|
25
|
+
and moves matching top-level files into them. Does NOT delete any files.`;
|
|
26
|
+
|
|
27
|
+
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
28
|
+
console.log(HELP);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const CATEGORIES = [
|
|
33
|
+
{
|
|
34
|
+
name: "Screenshots",
|
|
35
|
+
test: (name) =>
|
|
36
|
+
name.startsWith("Screenshot") || name.startsWith("Screen Shot"),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "Documents",
|
|
40
|
+
test: (_name, ext) =>
|
|
41
|
+
[
|
|
42
|
+
".pdf",
|
|
43
|
+
".doc",
|
|
44
|
+
".docx",
|
|
45
|
+
".txt",
|
|
46
|
+
".md",
|
|
47
|
+
".rtf",
|
|
48
|
+
".csv",
|
|
49
|
+
".xlsx",
|
|
50
|
+
].includes(ext),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "Images",
|
|
54
|
+
test: (_name, ext) =>
|
|
55
|
+
[".png", ".jpg", ".jpeg", ".gif", ".webp"].includes(ext),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "Archives",
|
|
59
|
+
test: (name, ext) =>
|
|
60
|
+
[".zip", ".rar"].includes(ext) || name.endsWith(".tar.gz"),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "Installers",
|
|
64
|
+
test: (_name, ext) => ext === ".dmg",
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function main() {
|
|
69
|
+
const dir = process.argv[2];
|
|
70
|
+
if (!dir) {
|
|
71
|
+
console.error("Usage: node scripts/organize-by-type.mjs <directory>");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
75
|
+
console.error(`Error: Directory not found: ${dir}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Create subdirectories
|
|
80
|
+
for (const cat of CATEGORIES) {
|
|
81
|
+
mkdirSync(join(dir, cat.name), { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let moved = 0;
|
|
85
|
+
for (const name of readdirSync(dir)) {
|
|
86
|
+
const fullPath = join(dir, name);
|
|
87
|
+
const stat = statSync(fullPath, { throwIfNoEntry: false });
|
|
88
|
+
if (!stat || !stat.isFile()) continue;
|
|
89
|
+
|
|
90
|
+
const ext = extname(name).toLowerCase();
|
|
91
|
+
for (const cat of CATEGORIES) {
|
|
92
|
+
if (cat.test(name, ext)) {
|
|
93
|
+
const dest = join(dir, cat.name, name);
|
|
94
|
+
renameSync(fullPath, dest);
|
|
95
|
+
console.log(`${name} -> ${cat.name}/`);
|
|
96
|
+
moved++;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`\nOrganization complete: ${dir} (${moved} files moved)`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
main();
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Summarize the contents of ~/Desktop/ and ~/Downloads/.
|
|
4
|
+
*
|
|
5
|
+
* Counts top-level files in both directories by type (Screenshots, PDFs,
|
|
6
|
+
* Images, Documents, Archives, Installers, Other) and prints a human-readable
|
|
7
|
+
* table for each. Used by the organize-files skill to preview directory
|
|
8
|
+
* contents before organizing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
12
|
+
import { extname, join } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
|
|
15
|
+
const HELP = `summarize — count files by type in ~/Desktop/ and ~/Downloads/
|
|
16
|
+
|
|
17
|
+
Usage: node scripts/summarize.mjs [-h|--help]
|
|
18
|
+
|
|
19
|
+
Prints a summary of file types found at the top level of each directory.`;
|
|
20
|
+
|
|
21
|
+
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
22
|
+
console.log(HELP);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const HOME = homedir();
|
|
27
|
+
|
|
28
|
+
function countFiles(dir) {
|
|
29
|
+
if (!existsSync(dir)) return null;
|
|
30
|
+
|
|
31
|
+
const counts = {
|
|
32
|
+
Screenshots: 0,
|
|
33
|
+
PDFs: 0,
|
|
34
|
+
Images: 0,
|
|
35
|
+
Documents: 0,
|
|
36
|
+
Archives: 0,
|
|
37
|
+
Installers: 0,
|
|
38
|
+
Other: 0,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
for (const name of readdirSync(dir)) {
|
|
42
|
+
const fullPath = join(dir, name);
|
|
43
|
+
const stat = statSync(fullPath, { throwIfNoEntry: false });
|
|
44
|
+
if (!stat || !stat.isFile()) continue;
|
|
45
|
+
if (name.startsWith(".")) continue;
|
|
46
|
+
|
|
47
|
+
const ext = extname(name).toLowerCase();
|
|
48
|
+
if (name.startsWith("Screenshot") || name.startsWith("Screen Shot")) {
|
|
49
|
+
counts.Screenshots++;
|
|
50
|
+
} else if (ext === ".pdf") {
|
|
51
|
+
counts.PDFs++;
|
|
52
|
+
} else if ([".png", ".jpg", ".jpeg", ".gif", ".webp"].includes(ext)) {
|
|
53
|
+
counts.Images++;
|
|
54
|
+
} else if (
|
|
55
|
+
[".doc", ".docx", ".txt", ".md", ".rtf", ".csv", ".xlsx"].includes(ext)
|
|
56
|
+
) {
|
|
57
|
+
counts.Documents++;
|
|
58
|
+
} else if ([".zip", ".rar"].includes(ext) || name.endsWith(".tar.gz")) {
|
|
59
|
+
counts.Archives++;
|
|
60
|
+
} else if (ext === ".dmg") {
|
|
61
|
+
counts.Installers++;
|
|
62
|
+
} else {
|
|
63
|
+
counts.Other++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return counts;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function main() {
|
|
71
|
+
for (const dirName of ["Desktop", "Downloads"]) {
|
|
72
|
+
const dir = join(HOME, dirName);
|
|
73
|
+
const counts = countFiles(dir);
|
|
74
|
+
if (!counts) continue;
|
|
75
|
+
|
|
76
|
+
console.log(`=== ${dirName} ===`);
|
|
77
|
+
for (const [label, count] of Object.entries(counts)) {
|
|
78
|
+
console.log(`${label.padEnd(12)} ${count}`);
|
|
79
|
+
}
|
|
80
|
+
console.log("");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main();
|
|
@@ -308,11 +308,11 @@ absolute paths: `[[People/Name]]`, `[[Organizations/Name]]`,
|
|
|
308
308
|
After processing each session, mark its files as processed:
|
|
309
309
|
|
|
310
310
|
```bash
|
|
311
|
-
|
|
311
|
+
node .claude/skills/extract-entities/scripts/state.mjs update \
|
|
312
312
|
"$HOME/Library/Application Support/hyprnote/sessions/{uuid}/_memo.md"
|
|
313
313
|
|
|
314
314
|
# Also mark _summary.md if it exists
|
|
315
|
-
|
|
315
|
+
node .claude/skills/extract-entities/scripts/state.mjs update \
|
|
316
316
|
"$HOME/Library/Application Support/hyprnote/sessions/{uuid}/_summary.md"
|
|
317
317
|
```
|
|
318
318
|
|
|
@@ -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
|
|
41
|
-
per event for attendees) and handles all data
|
|
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
|
-
|
|
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 (
|
|
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
|