@forwardimpact/outpost 3.11.0 → 3.12.1
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 +2 -2
- package/bin/fit-outpost.js +3 -2
- package/package.json +4 -4
- package/src/agent-path.js +10 -6
- package/src/agent-runner.js +28 -24
- package/src/index.js +2 -3
- package/src/kb-manager.js +21 -19
- package/src/outpost.js +37 -33
- package/src/posture.js +3 -3
- package/src/privilege.js +11 -11
- package/src/scheduler.js +14 -12
- package/src/socket-server.js +19 -18
- package/src/spawn-env.js +7 -6
- package/src/state-manager.js +2 -2
- package/templates/.claude/agents/chief-of-staff.md +22 -22
- package/templates/.claude/agents/concierge.md +19 -17
- package/templates/.claude/agents/head-hunter.md +27 -26
- package/templates/.claude/agents/librarian.md +17 -16
- package/templates/.claude/agents/postman.md +19 -18
- package/templates/.claude/agents/recruiter.md +31 -27
- package/templates/.claude/skills/anarlog-follow/SKILL.md +50 -49
- package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
- package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +4 -4
- package/templates/.claude/skills/anarlog-process/SKILL.md +35 -32
- package/templates/.claude/skills/anarlog-process/references/extraction.md +26 -22
- package/templates/.claude/skills/anarlog-process/references/sessions.md +10 -10
- package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +10 -9
- package/templates/.claude/skills/anarlog-trim/SKILL.md +43 -40
- package/templates/.claude/skills/candidate-report/SKILL.md +30 -28
- package/templates/.claude/skills/candidate-report/references/report-template.html +10 -10
- package/templates/.claude/skills/candidate-report/references/report.css +4 -4
- package/templates/.claude/skills/candidate-report/references/rubric.md +15 -14
- package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +2 -2
- package/templates/.claude/skills/changelog/SKILL.md +55 -50
- package/templates/.claude/skills/deck-create/SKILL.md +30 -30
- package/templates/.claude/skills/deck-create/references/slide.css +4 -4
- package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/deck-review/SKILL.md +59 -57
- package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
- package/templates/.claude/skills/deck-summarize/SKILL.md +49 -43
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +4 -3
- package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
- package/templates/.claude/skills/doc-collab/SKILL.md +25 -25
- package/templates/.claude/skills/doc-create/SKILL.md +21 -20
- package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/draft-emails/SKILL.md +37 -33
- package/templates/.claude/skills/draft-emails/references/template.md +2 -2
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +5 -4
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +11 -9
- package/templates/.claude/skills/extract-entities/SKILL.md +65 -65
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +5 -5
- package/templates/.claude/skills/extract-entities/references/conditions.md +14 -13
- package/templates/.claude/skills/extract-entities/references/content.md +10 -9
- package/templates/.claude/skills/extract-entities/references/links.md +5 -5
- package/templates/.claude/skills/extract-entities/references/recruitment.md +19 -18
- package/templates/.claude/skills/extract-entities/references/resolution.md +13 -13
- package/templates/.claude/skills/extract-entities/references/sources.md +8 -8
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +2 -2
- package/templates/.claude/skills/extract-entities/references/templates-priorities.md +2 -2
- package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
- package/templates/.claude/skills/meeting-prep/SKILL.md +25 -24
- package/templates/.claude/skills/organize-files/SKILL.md +24 -22
- package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +5 -4
- package/templates/.claude/skills/organize-files/scripts/summarize.mjs +4 -4
- package/templates/.claude/skills/person-identify/SKILL.md +24 -22
- package/templates/.claude/skills/person-identify/scripts/identify.sh +17 -17
- package/templates/.claude/skills/person-lookup/SKILL.md +28 -29
- package/templates/.claude/skills/person-lookup/scripts/lookup.sh +19 -16
- package/templates/.claude/skills/req-assess/SKILL.md +27 -27
- package/templates/.claude/skills/req-assess/references/interview-template.md +1 -1
- package/templates/.claude/skills/req-assess/references/panel-template.md +3 -2
- package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
- package/templates/.claude/skills/req-decide/SKILL.md +28 -25
- package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
- package/templates/.claude/skills/req-forget/SKILL.md +28 -26
- package/templates/.claude/skills/req-forget/references/classify.md +13 -13
- package/templates/.claude/skills/req-forget/references/locations.md +2 -2
- package/templates/.claude/skills/req-forget/references/report-template.md +1 -1
- package/templates/.claude/skills/req-scan/SKILL.md +21 -22
- package/templates/.claude/skills/req-scan/references/fallbacks.md +9 -9
- package/templates/.claude/skills/req-scan/references/filters.md +13 -13
- package/templates/.claude/skills/req-scan/references/sources.md +7 -7
- package/templates/.claude/skills/req-scan/references/state.md +1 -1
- package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
- package/templates/.claude/skills/req-screen/SKILL.md +31 -29
- package/templates/.claude/skills/req-screen/references/rubric.md +18 -18
- package/templates/.claude/skills/req-screen/references/template.md +4 -4
- package/templates/.claude/skills/req-track/SKILL.md +40 -40
- package/templates/.claude/skills/req-track/references/fields.md +19 -18
- package/templates/.claude/skills/req-track/references/signals.md +12 -12
- package/templates/.claude/skills/req-track/references/statuses.md +3 -2
- package/templates/.claude/skills/req-track/references/templates.md +4 -4
- package/templates/.claude/skills/req-workday/SKILL.md +24 -24
- package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
- package/templates/.claude/skills/req-workday/references/templates.md +13 -13
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +7 -7
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +19 -17
- package/templates/.claude/skills/send-chat/SKILL.md +47 -43
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +20 -19
- package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +7 -7
- package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +6 -6
- package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +9 -8
- package/templates/.claude/skills/sync-apple-mail/SKILL.md +17 -14
- package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +10 -10
- package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +9 -9
- package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +16 -13
- package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +3 -3
- package/templates/.claude/skills/sync-teams/SKILL.md +43 -43
- package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +22 -21
- package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +4 -4
- package/templates/.claude/skills/sync-teams/scripts/sync.mjs +4 -4
- package/templates/.claude/skills/upstream-instructions/SKILL.md +39 -40
- package/templates/.claude/skills/upstream-instructions/references/examples.md +2 -2
- package/templates/CLAUDE.md +40 -37
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Parse a Workday requisition export (.xlsx) and output structured JSON.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* candidate data.
|
|
5
|
+
* The script reads Sheet1 for requisition metadata. It reads the "Candidates"
|
|
6
|
+
* sheet for candidate data. It outputs a JSON object to stdout with:
|
|
7
7
|
* - requisition: { id, title, startDate, targetHireDate, location,
|
|
8
8
|
* hiringManager, recruiter }
|
|
9
9
|
* - candidates: [ { name, cleanName, stage, step, resumeFile, dateApplied,
|
|
@@ -54,7 +54,7 @@ try {
|
|
|
54
54
|
const filePath = process.argv[2];
|
|
55
55
|
const summaryMode = process.argv.includes("--summary");
|
|
56
56
|
|
|
57
|
-
/** Read a sheet by number (1-indexed) or name
|
|
57
|
+
/** Read a sheet by number (1-indexed) or name. Rows are arrays of strings. */
|
|
58
58
|
async function readSheet(file, sheet) {
|
|
59
59
|
const rows = await readXlsxFile(file, { sheet });
|
|
60
60
|
// Normalise null cells to empty strings to match previous behaviour
|
|
@@ -122,7 +122,7 @@ const candSheetName =
|
|
|
122
122
|
sheetNames[Math.min(2, sheetNames.length - 1)];
|
|
123
123
|
const candRows = await readSheet(filePath, candSheetName);
|
|
124
124
|
|
|
125
|
-
// Find the header row dynamically
|
|
125
|
+
// Find the header row dynamically. Look for a row that contains "Stage".
|
|
126
126
|
// Old format: row 3 (index 2). New format: row 8 (index 7).
|
|
127
127
|
let HEADER_ROW = 2;
|
|
128
128
|
for (let i = 0; i < Math.min(15, candRows.length); i++) {
|
|
@@ -134,8 +134,9 @@ for (let i = 0; i < Math.min(15, candRows.length); i++) {
|
|
|
134
134
|
const DATA_START = HEADER_ROW + 1;
|
|
135
135
|
|
|
136
136
|
// --- Build header-driven column index map ---
|
|
137
|
-
// Column layout varies between Workday exports
|
|
138
|
-
// or "Referred by" shift indices
|
|
137
|
+
// Column layout varies between Workday exports. Extra columns like "Jobs
|
|
138
|
+
// Applied to" or "Referred by" shift the indices. Map by header name to be
|
|
139
|
+
// resilient.
|
|
139
140
|
|
|
140
141
|
const headerRow = candRows[HEADER_ROW] || [];
|
|
141
142
|
const colMap = {};
|
|
@@ -174,16 +175,17 @@ for (let i = 0; i < headerRow.length; i++) {
|
|
|
174
175
|
const hdr = String(headerRow[i]).trim().toLowerCase();
|
|
175
176
|
const field = HEADER_ALIASES[hdr];
|
|
176
177
|
if (field) {
|
|
177
|
-
// "Job Application" appears twice (cols A and B)
|
|
178
|
-
// occurrence
|
|
178
|
+
// "Job Application" appears twice (cols A and B). Always take the latest
|
|
179
|
+
// occurrence. We then end up with the second one (index 1), which has the
|
|
180
|
+
// name.
|
|
179
181
|
colMap[field] = i;
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
|
|
183
|
-
// Fallback:
|
|
185
|
+
// Fallback: with no "name" column, use index 0 (new format) or 1 (old format)
|
|
184
186
|
if (colMap.name === undefined) colMap.name = 1;
|
|
185
|
-
// In new format there
|
|
186
|
-
// "always take latest" logic already handles this correctly.
|
|
187
|
+
// In the new format there is only one "Job Application" column (index 0).
|
|
188
|
+
// The "always take latest" logic already handles this correctly.
|
|
187
189
|
|
|
188
190
|
/** Get a cell value by field name, with fallback to empty string. */
|
|
189
191
|
function col(row, field) {
|
|
@@ -193,8 +195,8 @@ function col(row, field) {
|
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
/**
|
|
196
|
-
* Clean a candidate name
|
|
197
|
-
*
|
|
198
|
+
* Clean a candidate name. Strip annotations like (Prior Worker), (Internal),
|
|
199
|
+
* etc. Returns { cleanName, internalExternal }.
|
|
198
200
|
*/
|
|
199
201
|
function parseName(raw) {
|
|
200
202
|
const name = String(raw).trim();
|
|
@@ -212,18 +214,18 @@ function parseName(raw) {
|
|
|
212
214
|
return { cleanName: name, internalExternal: "" };
|
|
213
215
|
}
|
|
214
216
|
|
|
215
|
-
/** Detect source-based internal/external when name annotation is absent. */
|
|
217
|
+
/** Detect source-based internal/external when the name annotation is absent. */
|
|
216
218
|
function inferInternalExternal(source, nameAnnotation) {
|
|
217
219
|
if (nameAnnotation) return nameAnnotation;
|
|
218
220
|
if (/internal/i.test(source)) return "Internal";
|
|
219
221
|
return "External";
|
|
220
222
|
}
|
|
221
223
|
|
|
222
|
-
/** Format a date value (may be Date object or string). */
|
|
224
|
+
/** Format a date value (it may be a Date object or a string). */
|
|
223
225
|
function fmtDate(val) {
|
|
224
226
|
if (!val) return "";
|
|
225
227
|
if (val instanceof Date) {
|
|
226
|
-
// Use local date parts
|
|
228
|
+
// Use local date parts so a UTC offset does not shift the day
|
|
227
229
|
const y = val.getFullYear();
|
|
228
230
|
const m = String(val.getMonth() + 1).padStart(2, "0");
|
|
229
231
|
const d = String(val.getDate()).padStart(2, "0");
|
|
@@ -253,7 +255,7 @@ for (let i = DATA_START; i < candRows.length; i++) {
|
|
|
253
255
|
const rawName = String(col(row, "name") || "").trim();
|
|
254
256
|
const stage = String(col(row, "stage") || "").trim();
|
|
255
257
|
|
|
256
|
-
// Skip empty rows
|
|
258
|
+
// Skip empty rows. Stop at stage-summary rows (name present but no stage)
|
|
257
259
|
if (!rawName) continue;
|
|
258
260
|
if (!stage) break;
|
|
259
261
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: send-chat
|
|
3
|
-
description: Send messages to people
|
|
3
|
+
description: Send messages to people through chat platforms (e.g. Microsoft Teams, Slack) with browser automation. Resolves people by name from the knowledge graph, drafts messages for approval, and sends them through the web app. Use when the user asks to message, ping, or chat with someone.
|
|
4
4
|
compatibility:
|
|
5
5
|
requires:
|
|
6
6
|
- browser-automation
|
|
@@ -8,10 +8,10 @@ compatibility:
|
|
|
8
8
|
|
|
9
9
|
# Send Chat
|
|
10
10
|
|
|
11
|
-
Send chat messages to people
|
|
12
|
-
platform (Microsoft Teams, Slack, or similar).
|
|
13
|
-
the knowledge graph
|
|
14
|
-
without
|
|
11
|
+
Send chat messages to people with browser automation against a web-based chat
|
|
12
|
+
platform (Microsoft Teams, Slack, or similar). This skill resolves recipients by
|
|
13
|
+
name from the knowledge graph. The user can say "message Sarah about the
|
|
14
|
+
standup" without an exact display name.
|
|
15
15
|
|
|
16
16
|
## Trigger
|
|
17
17
|
|
|
@@ -19,37 +19,38 @@ Run when the user asks to:
|
|
|
19
19
|
|
|
20
20
|
- Send a message on Teams / Slack / chat
|
|
21
21
|
- Ping / chat / DM someone
|
|
22
|
-
- Follow up with someone
|
|
22
|
+
- Follow up with someone through chat
|
|
23
23
|
- Send a message about a topic
|
|
24
24
|
|
|
25
25
|
## Prerequisites
|
|
26
26
|
|
|
27
|
-
-
|
|
27
|
+
- The web app for the chat platform, open and authenticated in the browser
|
|
28
28
|
- Browser automation available (e.g. Chrome MCP, Playwright)
|
|
29
29
|
- Knowledge base populated with people notes
|
|
30
30
|
|
|
31
31
|
## Critical: Always Look Up Context First
|
|
32
32
|
|
|
33
|
-
**BEFORE
|
|
33
|
+
**BEFORE you message anyone, you MUST look up the person in the knowledge
|
|
34
|
+
base.**
|
|
34
35
|
|
|
35
36
|
When the user mentions ANY person:
|
|
36
37
|
|
|
37
38
|
1. **STOP** — Do not open the chat platform yet
|
|
38
39
|
2. **SEARCH** — Look them up: `rg -l "{name}" Knowledge/People/`
|
|
39
40
|
3. **READ** — Read their note to understand context, role, recent interactions
|
|
40
|
-
4. **UNDERSTAND** — Know who they are
|
|
41
|
+
4. **UNDERSTAND** — Know who they are and what you work on together
|
|
41
42
|
5. **THEN PROCEED** — Only now compose the message and use browser automation
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
You need this context to:
|
|
44
45
|
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
46
|
+
- Find the right person if the name is ambiguous
|
|
47
|
+
- Draft an appropriate message if the user gave a loose prompt
|
|
48
|
+
- Know the person's role and relationship for tone
|
|
48
49
|
|
|
49
|
-
##
|
|
50
|
+
## Resolve People
|
|
50
51
|
|
|
51
|
-
The user will refer to people by first name, last name, or nickname. Resolve
|
|
52
|
-
a full name
|
|
52
|
+
The user will refer to people by first name, last name, or nickname. Resolve the
|
|
53
|
+
reference to a full name with the knowledge graph:
|
|
53
54
|
|
|
54
55
|
```bash
|
|
55
56
|
# Find person by partial name
|
|
@@ -59,16 +60,16 @@ rg -l -i "{name}" Knowledge/People/
|
|
|
59
60
|
cat "Knowledge/People/{Candidate}.md"
|
|
60
61
|
```
|
|
61
62
|
|
|
62
|
-
**If ambiguous** (multiple matches), ask the user which person they mean
|
|
63
|
+
**If ambiguous** (multiple matches), ask the user which person they mean. List
|
|
63
64
|
the matches with roles/orgs to help them pick.
|
|
64
65
|
|
|
65
|
-
**If no match**, tell the user
|
|
66
|
-
|
|
66
|
+
**If no match**, tell the user this person is not in the knowledge base. Ask for
|
|
67
|
+
their full name as it appears in the chat platform.
|
|
67
68
|
|
|
68
|
-
##
|
|
69
|
+
## Compose the Message
|
|
69
70
|
|
|
70
|
-
**
|
|
71
|
-
can review and edit the exact message before it
|
|
71
|
+
**You MUST draft every message as a text file first.** This makes sure the user
|
|
72
|
+
can review and edit the exact message before you send it.
|
|
72
73
|
|
|
73
74
|
### Draft Workflow
|
|
74
75
|
|
|
@@ -92,32 +93,33 @@ Date: {YYYY-MM-DD}
|
|
|
92
93
|
{message body}
|
|
93
94
|
```
|
|
94
95
|
|
|
95
|
-
The message body
|
|
96
|
+
The message body is everything below the `---` separator. Paste that body into
|
|
96
97
|
the chat.
|
|
97
98
|
|
|
98
99
|
**Message guidelines:**
|
|
99
100
|
|
|
100
|
-
- Match the user's usual tone
|
|
101
|
-
|
|
101
|
+
- Match the user's usual tone. Use casual tone for peers and professional tone
|
|
102
|
+
for leadership
|
|
103
|
+
- Keep it concise. Chat is informal. It is not email
|
|
102
104
|
- Reference specific context naturally (project names, recent decisions)
|
|
103
|
-
- If the user
|
|
105
|
+
- If the user gives exact words, use them verbatim
|
|
104
106
|
- If the user said "ping {name}" without detail, ask what they want to say
|
|
105
|
-
- Draft one message based on context
|
|
107
|
+
- Draft one message based on context. Do not offer multiple options
|
|
106
108
|
- **Keep messages on a single line with no formatting.** No line breaks, no
|
|
107
109
|
markdown. Use inline separators (e.g. `•`, `—`) to keep structure. Multi-line
|
|
108
|
-
formatting is unreliable
|
|
110
|
+
formatting is unreliable through browser automation.
|
|
109
111
|
|
|
110
112
|
## Browser Automation Flow
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
After the user approves the draft, send it as a **single submission**. Paste the
|
|
115
|
+
entire message at once. Do not type it line by line.
|
|
114
116
|
|
|
115
117
|
### Step 1: Identify the Chat Platform
|
|
116
118
|
|
|
117
119
|
Check which platform is available:
|
|
118
120
|
|
|
119
|
-
- Look for an open tab
|
|
120
|
-
- If no tab is open, ask the user which platform to use
|
|
121
|
+
- Look for an open tab that matches the configured chat URL
|
|
122
|
+
- If no tab is open, ask the user which platform to use. Then navigate to it
|
|
121
123
|
|
|
122
124
|
### Step 2: Open a Chat with the Recipient
|
|
123
125
|
|
|
@@ -126,7 +128,7 @@ Check which platform is available:
|
|
|
126
128
|
3. Wait for search results to populate (take a screenshot to verify)
|
|
127
129
|
4. Click the correct person from the results
|
|
128
130
|
|
|
129
|
-
If the person
|
|
131
|
+
If the person does not appear in search, tell the user. They may not be in the
|
|
130
132
|
same organization.
|
|
131
133
|
|
|
132
134
|
### Step 3: Send the Approved Message
|
|
@@ -150,21 +152,23 @@ on the person's knowledge note:
|
|
|
150
152
|
|
|
151
153
|
- **Platform not loaded / auth required:** Tell the user to sign in first, then
|
|
152
154
|
retry
|
|
153
|
-
- **Person not found in search:** Report back
|
|
154
|
-
different display name. Ask the user for the exact name
|
|
155
|
+
- **Person not found in search:** Report back. They may be external, or they may
|
|
156
|
+
use a different display name. Ask the user for the exact name
|
|
155
157
|
- **Chat already open:** If a chat with this person is already visible, use it
|
|
156
158
|
directly
|
|
157
|
-
- **UI not as expected:** Take a screenshot and describe what you see.
|
|
159
|
+
- **UI not as expected:** Take a screenshot and describe what you see. Do not
|
|
158
160
|
click blindly
|
|
159
161
|
|
|
160
162
|
## Constraints
|
|
161
163
|
|
|
162
|
-
- **Always confirm before
|
|
163
|
-
approval
|
|
164
|
-
- **One message at a time.**
|
|
165
|
-
|
|
164
|
+
- **Always confirm before you send.** Never send a message without explicit user
|
|
165
|
+
approval. This is a hard requirement
|
|
166
|
+
- **One message at a time.** Do not batch-send to multiple people. Confirm each
|
|
167
|
+
one first
|
|
166
168
|
- **No file attachments.** This skill handles text messages only
|
|
167
|
-
- **No group chats.**
|
|
168
|
-
- **No message deletion or editing.**
|
|
169
|
+
- **No group chats.** This skill targets 1:1 chats only
|
|
170
|
+
- **No message deletion or editing.** After you send a message, you cannot
|
|
171
|
+
delete it or edit it
|
|
169
172
|
- **Respect ethics rules.** Never send messages that contain personal judgments,
|
|
170
|
-
gossip, or sensitive information
|
|
173
|
+
gossip, or sensitive information. The ethics policy of the knowledge base
|
|
174
|
+
requires this
|
|
@@ -7,8 +7,8 @@ compatibility: Requires macOS with Apple Calendar configured and Full Disk Acces
|
|
|
7
7
|
# Sync Apple Calendar
|
|
8
8
|
|
|
9
9
|
Sync calendar events from the macOS Calendar app's local SQLite database into
|
|
10
|
-
`~/.cache/fit/outpost/apple_calendar/` as JSON files. This is an automated
|
|
11
|
-
|
|
10
|
+
`~/.cache/fit/outpost/apple_calendar/` as JSON files. This is an automated skill
|
|
11
|
+
in the data pipeline. It ingests raw calendar data that other skills (like
|
|
12
12
|
`extract-entities` and `meeting-prep`) consume downstream.
|
|
13
13
|
|
|
14
14
|
## Trigger
|
|
@@ -38,8 +38,8 @@ their calendar.
|
|
|
38
38
|
## Implementation
|
|
39
39
|
|
|
40
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)
|
|
42
|
-
|
|
41
|
+
process invocations (one per event for attendees). It also transforms all the
|
|
42
|
+
data in one pass:
|
|
43
43
|
|
|
44
44
|
```text
|
|
45
45
|
node scripts/sync.mjs [--days N]
|
|
@@ -60,8 +60,9 @@ The script:
|
|
|
60
60
|
## Database Schema
|
|
61
61
|
|
|
62
62
|
See [references/SCHEMA.md](references/SCHEMA.md) for the complete Apple Calendar
|
|
63
|
-
SQLite schema
|
|
64
|
-
|
|
63
|
+
SQLite schema. It gives the table structures, the column names, and the
|
|
64
|
+
important caveats. For example, Identity uses `address` and has no `email`
|
|
65
|
+
column. Participant has no `display_name`.
|
|
65
66
|
|
|
66
67
|
## Output Format
|
|
67
68
|
|
|
@@ -98,10 +99,10 @@ Each `{event_id}.json` file:
|
|
|
98
99
|
- Database locked → wait 2 seconds, retry once
|
|
99
100
|
- Skip events with no summary (likely cancelled or placeholder)
|
|
100
101
|
|
|
101
|
-
##
|
|
102
|
+
## Query Events
|
|
102
103
|
|
|
103
|
-
After
|
|
104
|
-
**Agents should use this script
|
|
104
|
+
After the sync, use the query script to filter events by date or time window.
|
|
105
|
+
**Agents should use this script. Do not write bespoke calendar parsers.**
|
|
105
106
|
|
|
106
107
|
```text
|
|
107
108
|
node scripts/query.mjs [options]
|
|
@@ -109,13 +110,13 @@ node scripts/query.mjs [options]
|
|
|
109
110
|
|
|
110
111
|
### Time filters (combinable)
|
|
111
112
|
|
|
112
|
-
| Flag | Description
|
|
113
|
-
| ------------------------------- |
|
|
114
|
-
| `--today` | Events
|
|
115
|
-
| `--tomorrow` | Events
|
|
116
|
-
| `--upcoming 2h` | Events
|
|
117
|
-
| `--date 2026-03-09` | Events on a specific date
|
|
118
|
-
| `--range 2026-03-09 2026-03-11` | Events between two dates (inclusive)
|
|
113
|
+
| Flag | Description |
|
|
114
|
+
| ------------------------------- | --------------------------------------------------------------- |
|
|
115
|
+
| `--today` | Events that start today (default if no filter given) |
|
|
116
|
+
| `--tomorrow` | Events that start tomorrow |
|
|
117
|
+
| `--upcoming 2h` | Events that start within the interval (e.g., `2h`, `30m`, `1d`) |
|
|
118
|
+
| `--date 2026-03-09` | Events on a specific date |
|
|
119
|
+
| `--range 2026-03-09 2026-03-11` | Events between two dates (inclusive) |
|
|
119
120
|
|
|
120
121
|
### Output options
|
|
121
122
|
|
|
@@ -144,6 +145,6 @@ node scripts/query.mjs --today --json | node -e "process.stdin.on('data',d=>cons
|
|
|
144
145
|
## Constraints
|
|
145
146
|
|
|
146
147
|
- Open database read-only (`readOnly: true`)
|
|
147
|
-
- This sync is stateless
|
|
148
|
-
- All-day events may have null end times
|
|
149
|
-
- All-day events have timezone `_float
|
|
148
|
+
- This sync is stateless. It always queries the current sliding window
|
|
149
|
+
- All-day events may have null end times. Use the start date as the end date
|
|
150
|
+
- All-day events have timezone `_float`. Omit the timezone from the output
|
|
@@ -19,7 +19,7 @@ column names (verified on macOS Sonoma+).
|
|
|
19
19
|
| `organizer_id` | INTEGER | FK → Identity.ROWID |
|
|
20
20
|
| `calendar_id` | INTEGER | FK → Calendar.ROWID |
|
|
21
21
|
| `has_attendees` | INTEGER | 1 = event has attendees |
|
|
22
|
-
| `conference_url` | TEXT | Video call URL (often null
|
|
22
|
+
| `conference_url` | TEXT | Video call URL (often null). Check description too |
|
|
23
23
|
| `entity_type` | INTEGER | 2 = calendar event |
|
|
24
24
|
|
|
25
25
|
## Identity (organizer info)
|
|
@@ -28,12 +28,12 @@ column names (verified on macOS Sonoma+).
|
|
|
28
28
|
| -------------- | ---- | ------------------------------------------------------------- |
|
|
29
29
|
| `display_name` | TEXT | Full name (e.g., `"Chen, Sarah"`) |
|
|
30
30
|
| `address` | TEXT | Email with `mailto:` prefix (e.g., `"mailto:sarah@acme.com"`) |
|
|
31
|
-
| `first_name` | TEXT | Usually null
|
|
32
|
-
| `last_name` | TEXT | Usually null
|
|
31
|
+
| `first_name` | TEXT | Usually null. `display_name` is the reliable field |
|
|
32
|
+
| `last_name` | TEXT | Usually null. `display_name` is the reliable field |
|
|
33
33
|
|
|
34
34
|
**IMPORTANT:** Identity does NOT have an `email` column. Use `address` and strip
|
|
35
|
-
the `mailto:` prefix. Use `display_name` for the name
|
|
36
|
-
`
|
|
35
|
+
the `mailto:` prefix. Use `display_name` for the name. Do not use `first_name`
|
|
36
|
+
or `last_name`. They are typically null.
|
|
37
37
|
|
|
38
38
|
## Participant (attendees and organizer)
|
|
39
39
|
|
|
@@ -49,8 +49,8 @@ the `mailto:` prefix. Use `display_name` for the name (not
|
|
|
49
49
|
| `is_self` | INTEGER | 1 = this is the calendar owner |
|
|
50
50
|
|
|
51
51
|
**IMPORTANT:** Participant does NOT have `display_name`, `first_name`, or
|
|
52
|
-
`last_name` columns. To get the attendee's name, JOIN with Identity
|
|
53
|
-
`identity_id`. There is NO `Attendee` table
|
|
52
|
+
`last_name` columns. To get the attendee's name, JOIN with Identity through
|
|
53
|
+
`identity_id`. There is NO `Attendee` table. Use `Participant` only.
|
|
54
54
|
|
|
55
55
|
### EKParticipantStatus mapping
|
|
56
56
|
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Query synced Apple Calendar events by date/time window.
|
|
4
4
|
*
|
|
5
5
|
* Reads JSON event files from ~/.cache/fit/outpost/apple_calendar/ and filters
|
|
6
|
-
* them by date range, time window, or upcoming interval.
|
|
7
|
-
*
|
|
6
|
+
* them by date range, time window, or upcoming interval. Agents then do not
|
|
7
|
+
* need to write their own calendar-parsing scripts.
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
10
|
* node scripts/query.mjs --today Today's events
|
|
@@ -31,9 +31,9 @@ Usage:
|
|
|
31
31
|
node scripts/query.mjs [options]
|
|
32
32
|
|
|
33
33
|
Time filters (combinable):
|
|
34
|
-
--today Events
|
|
35
|
-
--tomorrow Events
|
|
36
|
-
--upcoming <interval> Events
|
|
34
|
+
--today Events that start today
|
|
35
|
+
--tomorrow Events that start tomorrow
|
|
36
|
+
--upcoming <interval> Events that start within interval (e.g., 2h, 30m, 1d)
|
|
37
37
|
--date <YYYY-MM-DD> Events on a specific date
|
|
38
38
|
--range <start> <end> Events between two dates (inclusive)
|
|
39
39
|
|
|
@@ -42,7 +42,7 @@ Output options:
|
|
|
42
42
|
--include-all-day Include all-day events (excluded by default)
|
|
43
43
|
--no-attendees Omit attendee names from output
|
|
44
44
|
|
|
45
|
-
Defaults to --today
|
|
45
|
+
Defaults to --today when you give no time filter.`);
|
|
46
46
|
process.exit(0);
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Sync Apple Calendar events to ~/.cache/fit/outpost/apple_calendar/ as JSON.
|
|
4
4
|
*
|
|
5
|
-
* Queries the macOS Calendar SQLite database (
|
|
6
|
-
* sliding window
|
|
7
|
-
* JSON file per event
|
|
8
|
-
*
|
|
5
|
+
* Queries the macOS Calendar SQLite database (through node:sqlite) for events
|
|
6
|
+
* in a sliding window. The window runs from N days in the past to 14 days in
|
|
7
|
+
* the future. Writes one JSON file per event. Removes the files for events
|
|
8
|
+
* outside the window. Fetches attendee details (name, email, status, role) in
|
|
9
|
+
* one batch and includes them.
|
|
9
10
|
*
|
|
10
11
|
* Requires macOS with Calendar app configured and Full Disk Access granted.
|
|
11
12
|
*/
|
|
@@ -74,7 +75,7 @@ function findDb() {
|
|
|
74
75
|
const db = DB_PATHS.find((p) => existsSync(p));
|
|
75
76
|
if (!db) {
|
|
76
77
|
console.error(
|
|
77
|
-
"Error: Apple Calendar database not found.
|
|
78
|
+
"Error: Apple Calendar database not found. Check that Calendar is configured.",
|
|
78
79
|
);
|
|
79
80
|
process.exit(1);
|
|
80
81
|
}
|
|
@@ -82,7 +83,7 @@ function findDb() {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
|
-
* Open the database in read-only mode
|
|
86
|
+
* Open the database in read-only mode. Retry once if the database is locked.
|
|
86
87
|
* @param {string} dbPath
|
|
87
88
|
* @returns {import("node:sqlite").DatabaseSync}
|
|
88
89
|
*/
|
|
@@ -114,7 +115,7 @@ function query(db, sql) {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
/**
|
|
117
|
-
* Convert Core Data timestamp to ISO 8601.
|
|
118
|
+
* Convert a Core Data timestamp to ISO 8601.
|
|
118
119
|
* @param {number | null} ts - Seconds since 2001-01-01
|
|
119
120
|
* @param {string | null} tzName
|
|
120
121
|
* @returns {string | null}
|
|
@@ -138,7 +139,7 @@ function coredataToIso(ts, tzName) {
|
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
/**
|
|
141
|
-
* Get UTC offset string for a timezone at a given instant.
|
|
142
|
+
* Get the UTC offset string for a timezone at a given instant.
|
|
142
143
|
* @param {Date} dt
|
|
143
144
|
* @param {string} tzName
|
|
144
145
|
* @returns {string} e.g. "+02:00" or "-05:00"
|
|
@@ -7,8 +7,8 @@ compatibility: Requires macOS with Apple Mail configured and Full Disk Access gr
|
|
|
7
7
|
# Sync Apple Mail
|
|
8
8
|
|
|
9
9
|
Sync email threads from the macOS Mail app's local SQLite database into
|
|
10
|
-
`~/.cache/fit/outpost/apple_mail/` as markdown files. This is an automated
|
|
11
|
-
|
|
10
|
+
`~/.cache/fit/outpost/apple_mail/` as markdown files. This is an automated skill
|
|
11
|
+
in the data pipeline. It ingests raw email data that other skills (like
|
|
12
12
|
`extract-entities`) consume downstream.
|
|
13
13
|
|
|
14
14
|
## Trigger
|
|
@@ -46,7 +46,7 @@ their email.
|
|
|
46
46
|
## Implementation
|
|
47
47
|
|
|
48
48
|
Run the sync as a single Node.js script with embedded SQLite. This avoids N+1
|
|
49
|
-
process invocations
|
|
49
|
+
process invocations. It also transforms all the data in one pass:
|
|
50
50
|
|
|
51
51
|
```text
|
|
52
52
|
node scripts/sync.mjs [--days N]
|
|
@@ -60,27 +60,29 @@ The script:
|
|
|
60
60
|
2. Loads last sync timestamp (or defaults to `--days` days ago for first sync)
|
|
61
61
|
3. Discovers the thread grouping column (`conversation_id` or `thread_id`)
|
|
62
62
|
4. Loads last-seen ROWID (or defaults to 0 for first sync)
|
|
63
|
-
5. Finds threads with new messages since last sync (up to 500)
|
|
64
|
-
timestamp and ROWID to catch late
|
|
65
|
-
delay may have `date_received` before the last sync timestamp
|
|
66
|
-
ROWID
|
|
63
|
+
5. Finds threads with new messages since last sync (up to 500). It uses both the
|
|
64
|
+
timestamp and the ROWID to catch emails that arrive late. An email downloaded
|
|
65
|
+
after a delay may have a `date_received` before the last sync timestamp. But
|
|
66
|
+
its ROWID is higher than the last-seen ROWID
|
|
67
67
|
6. For each thread: fetches messages, batch-fetches recipients and attachment
|
|
68
|
-
metadata, parses `.emlx` files for full email bodies (
|
|
68
|
+
metadata, parses `.emlx` files for full email bodies (with a fallback to
|
|
69
69
|
database summaries), copies attachment files to the output directory
|
|
70
70
|
7. Writes one markdown file per thread to `~/.cache/fit/outpost/apple_mail/`
|
|
71
71
|
8. Updates sync state (timestamp and max ROWID)
|
|
72
72
|
9. Reports summary (threads processed, files written)
|
|
73
73
|
|
|
74
74
|
The script imports `scripts/parse-emlx.mjs` to extract plain text bodies from
|
|
75
|
-
`.emlx` / `.partial.emlx` files
|
|
75
|
+
`.emlx` / `.partial.emlx` files. The parser strips the tags from HTML-only
|
|
76
|
+
emails.
|
|
76
77
|
|
|
77
78
|
## Database Schema
|
|
78
79
|
|
|
79
80
|
See [references/SCHEMA.md](references/SCHEMA.md) for the complete Apple Mail
|
|
80
|
-
SQLite schema
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`message_id
|
|
81
|
+
SQLite schema. It gives the table structures, the column names, and the
|
|
82
|
+
important caveats. For example, `date_received` holds Unix timestamps. It does
|
|
83
|
+
not hold Core Data timestamps. `addresses.comment` holds display names. The
|
|
84
|
+
`recipients` columns are `message` and `address`. They are not `message_id` and
|
|
85
|
+
`address_id`.
|
|
84
86
|
|
|
85
87
|
## Output Format
|
|
86
88
|
|
|
@@ -139,7 +141,8 @@ Rules:
|
|
|
139
141
|
- `.emlx` parse error → fall back to database summary field
|
|
140
142
|
- HTML-only email → strip tags and use as plain text body (handled by
|
|
141
143
|
parse-emlx.mjs)
|
|
142
|
-
- `find` timeout → skip that message's body
|
|
144
|
+
- `find` timeout → skip that message's body and use the summary. The attachment
|
|
145
|
+
index stays empty
|
|
143
146
|
- Attachment file not found on disk → listed as `*(not available)*` in markdown
|
|
144
147
|
- Attachment copy fails (permissions, disk full) → listed as `*(not available)*`
|
|
145
148
|
- Filename collision across messages → prefixed with `{message_id}_`
|
|
@@ -25,8 +25,8 @@ Typical path: `~/Library/Mail/V10/MailData/Envelope Index`
|
|
|
25
25
|
| `flagged` | INTEGER | 1 = flagged |
|
|
26
26
|
|
|
27
27
|
**IMPORTANT:** `date_received` stores **Unix timestamps** (seconds since
|
|
28
|
-
1970-01-01 UTC)
|
|
29
|
-
apply Core Data conversion.
|
|
28
|
+
1970-01-01 UTC). It does NOT store Core Data timestamps, which use the
|
|
29
|
+
2001-01-01 epoch. Do NOT apply Core Data conversion.
|
|
30
30
|
|
|
31
31
|
## addresses (sender and recipient addresses)
|
|
32
32
|
|
|
@@ -36,8 +36,8 @@ apply Core Data conversion.
|
|
|
36
36
|
| `address` | TEXT | Email address |
|
|
37
37
|
| `comment` | TEXT | Display name (e.g., `"Chen, Sarah"`) |
|
|
38
38
|
|
|
39
|
-
**IMPORTANT:** The display name is in `comment
|
|
40
|
-
column.
|
|
39
|
+
**IMPORTANT:** The display name is in `comment`. There is no `name` column and
|
|
40
|
+
no `display_name` column.
|
|
41
41
|
|
|
42
42
|
## subjects
|
|
43
43
|
|
|
@@ -46,8 +46,8 @@ column.
|
|
|
46
46
|
| `ROWID` | INTEGER | Primary key |
|
|
47
47
|
| `subject` | TEXT | Base subject text |
|
|
48
48
|
|
|
49
|
-
Note: `subject_prefix` (Re:, Fwd:, etc.)
|
|
50
|
-
|
|
49
|
+
Note: the `messages` table stores `subject_prefix` (Re:, Fwd:, etc.) directly.
|
|
50
|
+
This table does not store it.
|
|
51
51
|
|
|
52
52
|
## recipients (To/Cc/Bcc per message)
|
|
53
53
|
|
|
@@ -59,8 +59,8 @@ directly, not here.
|
|
|
59
59
|
| `type` | INTEGER | 0 = To, 1 = Cc, 2 = Bcc |
|
|
60
60
|
| `position` | INTEGER | Order within the type group |
|
|
61
61
|
|
|
62
|
-
**IMPORTANT:**
|
|
63
|
-
`address_id
|
|
62
|
+
**IMPORTANT:** The column names are `message` and `address`. They are not
|
|
63
|
+
`message_id` or `address_id`.
|
|
64
64
|
|
|
65
65
|
## summaries (Apple Intelligence email summaries)
|
|
66
66
|
|
|
@@ -99,8 +99,8 @@ Use case-insensitive `LIKE` patterns to match both:
|
|
|
99
99
|
**Constraints:** `UNIQUE(message, attachment_id)` — each attachment within a
|
|
100
100
|
message has a unique identifier.
|
|
101
101
|
|
|
102
|
-
**IMPORTANT:**
|
|
103
|
-
|
|
102
|
+
**IMPORTANT:** The column is `message`. It is not `message_id`. This matches the
|
|
103
|
+
convention of the `recipients` table.
|
|
104
104
|
|
|
105
105
|
### Filesystem mapping
|
|
106
106
|
|