@forwardimpact/outpost 3.0.3
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/LICENSE +201 -0
- package/README.md +15 -0
- package/bin/fit-outpost.js +3 -0
- package/config/scheduler.json +34 -0
- package/package.json +63 -0
- package/src/agent-runner.js +189 -0
- package/src/index.js +5 -0
- package/src/kb-manager.js +228 -0
- package/src/outpost.js +417 -0
- package/src/scheduler.js +195 -0
- package/src/socket-server.js +336 -0
- package/src/state-manager.js +126 -0
- package/templates/.claude/agents/chief-of-staff.md +58 -0
- package/templates/.claude/agents/concierge.md +46 -0
- package/templates/.claude/agents/head-hunter.md +57 -0
- package/templates/.claude/agents/librarian.md +60 -0
- package/templates/.claude/agents/postman.md +47 -0
- package/templates/.claude/agents/recruiter.md +63 -0
- package/templates/.claude/settings.json +88 -0
- package/templates/.claude/skills/candidate-report/SKILL.md +139 -0
- package/templates/.claude/skills/candidate-report/references/report-template.html +203 -0
- package/templates/.claude/skills/candidate-report/references/report.css +438 -0
- package/templates/.claude/skills/candidate-report/references/rubric.md +76 -0
- package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +48 -0
- package/templates/.claude/skills/deck-create/SKILL.md +75 -0
- package/templates/.claude/skills/deck-create/references/slide.css +35 -0
- package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +47 -0
- package/templates/.claude/skills/deck-summarize/SKILL.md +139 -0
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +85 -0
- package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +210 -0
- package/templates/.claude/skills/doc-collab/SKILL.md +112 -0
- package/templates/.claude/skills/doc-create/SKILL.md +105 -0
- package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +50 -0
- package/templates/.claude/skills/draft-emails/SKILL.md +141 -0
- package/templates/.claude/skills/draft-emails/references/template.md +26 -0
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +66 -0
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +139 -0
- package/templates/.claude/skills/extract-entities/SKILL.md +187 -0
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +14 -0
- package/templates/.claude/skills/extract-entities/references/conditions.md +55 -0
- package/templates/.claude/skills/extract-entities/references/content.md +104 -0
- package/templates/.claude/skills/extract-entities/references/links.md +48 -0
- package/templates/.claude/skills/extract-entities/references/recruitment.md +57 -0
- package/templates/.claude/skills/extract-entities/references/resolution.md +71 -0
- package/templates/.claude/skills/extract-entities/references/sources.md +70 -0
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +36 -0
- package/templates/.claude/skills/extract-entities/references/templates-goals-priorities.md +63 -0
- package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +67 -0
- package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +68 -0
- package/templates/.claude/skills/extract-entities/scripts/state.mjs +131 -0
- package/templates/.claude/skills/hyprnote-follow/SKILL.md +174 -0
- package/templates/.claude/skills/hyprnote-follow/references/coaching.md +118 -0
- package/templates/.claude/skills/hyprnote-follow/references/debrief.md +59 -0
- package/templates/.claude/skills/hyprnote-follow/references/meeting-types.md +14 -0
- package/templates/.claude/skills/hyprnote-follow/scripts/follow.mjs +243 -0
- package/templates/.claude/skills/hyprnote-process/SKILL.md +147 -0
- package/templates/.claude/skills/hyprnote-process/references/extraction.md +84 -0
- package/templates/.claude/skills/hyprnote-process/references/sessions.md +81 -0
- package/templates/.claude/skills/hyprnote-process/scripts/scan.mjs +246 -0
- package/templates/.claude/skills/hyprnote-trim/SKILL.md +177 -0
- package/templates/.claude/skills/manage-tasks/SKILL.md +152 -0
- package/templates/.claude/skills/manage-tasks/references/format.md +73 -0
- package/templates/.claude/skills/meeting-prep/SKILL.md +180 -0
- package/templates/.claude/skills/organize-files/SKILL.md +146 -0
- package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +105 -0
- package/templates/.claude/skills/organize-files/scripts/summarize.mjs +91 -0
- package/templates/.claude/skills/req-assess/SKILL.md +137 -0
- package/templates/.claude/skills/req-assess/references/interview-template.md +62 -0
- package/templates/.claude/skills/req-assess/references/panel-template.md +117 -0
- package/templates/.claude/skills/req-assess/references/rubric.md +65 -0
- package/templates/.claude/skills/req-decide/SKILL.md +163 -0
- package/templates/.claude/skills/req-decide/references/rubric.md +63 -0
- package/templates/.claude/skills/req-decide/references/template.md +118 -0
- package/templates/.claude/skills/req-forget/SKILL.md +154 -0
- package/templates/.claude/skills/req-forget/references/classify.md +23 -0
- package/templates/.claude/skills/req-forget/references/locations.md +59 -0
- package/templates/.claude/skills/req-forget/references/report-template.md +67 -0
- package/templates/.claude/skills/req-scan/SKILL.md +145 -0
- package/templates/.claude/skills/req-scan/references/fallbacks.md +76 -0
- package/templates/.claude/skills/req-scan/references/filters.md +83 -0
- package/templates/.claude/skills/req-scan/references/sources.md +76 -0
- package/templates/.claude/skills/req-scan/references/state.md +54 -0
- package/templates/.claude/skills/req-scan/references/template.md +36 -0
- package/templates/.claude/skills/req-scan/scripts/state.mjs +396 -0
- package/templates/.claude/skills/req-screen/SKILL.md +151 -0
- package/templates/.claude/skills/req-screen/references/rubric.md +94 -0
- package/templates/.claude/skills/req-screen/references/template.md +91 -0
- package/templates/.claude/skills/req-track/SKILL.md +189 -0
- package/templates/.claude/skills/req-track/references/fields.md +114 -0
- package/templates/.claude/skills/req-track/references/signals.md +42 -0
- package/templates/.claude/skills/req-track/references/statuses.md +32 -0
- package/templates/.claude/skills/req-track/references/templates.md +127 -0
- package/templates/.claude/skills/req-workday/SKILL.md +162 -0
- package/templates/.claude/skills/req-workday/references/field-mapping.md +40 -0
- package/templates/.claude/skills/req-workday/references/status-mapping.md +38 -0
- package/templates/.claude/skills/req-workday/references/templates.md +126 -0
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +56 -0
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +313 -0
- package/templates/.claude/skills/send-chat/SKILL.md +170 -0
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +145 -0
- package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +80 -0
- package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +307 -0
- package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +332 -0
- package/templates/.claude/skills/sync-apple-mail/SKILL.md +151 -0
- package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +115 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +389 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +372 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +239 -0
- package/templates/.claude/skills/sync-teams/SKILL.md +177 -0
- package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +147 -0
- package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +288 -0
- package/templates/.claude/skills/sync-teams/scripts/sync.mjs +381 -0
- package/templates/.claude/skills/upstream-skill/SKILL.md +130 -0
- package/templates/.claude/skills/upstream-skill/references/examples.md +80 -0
- package/templates/.claude/skills/weekly-update/SKILL.md +141 -0
- package/templates/.claude/skills/weekly-update/references/template.md +55 -0
- package/templates/CLAUDE.md +134 -0
- package/templates/USER.md +3 -0
- package/templates/apm.yml +5 -0
- package/templates/knowledge/Briefings/.gitkeep +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doc-collab
|
|
3
|
+
description: Help the user create, edit, and refine documents in the knowledge base. Use when the user asks to create, edit, review, or collaborate on a document. Supports direct editing and approval-based workflows with knowledge base context for entity references.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Document Collaboration
|
|
7
|
+
|
|
8
|
+
Help the user create, edit, and refine documents in the knowledge base. Supports
|
|
9
|
+
direct editing and approval-based workflows. Always uses knowledge base context
|
|
10
|
+
for entity references.
|
|
11
|
+
|
|
12
|
+
## Trigger
|
|
13
|
+
|
|
14
|
+
Run when the user asks to create, edit, review, or collaborate on a document.
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
- Knowledge base directory exists
|
|
19
|
+
|
|
20
|
+
## Inputs
|
|
21
|
+
|
|
22
|
+
- User's editing instructions
|
|
23
|
+
- `knowledge/` — existing notes and documents
|
|
24
|
+
- Document to edit (user-specified or searched)
|
|
25
|
+
|
|
26
|
+
## Outputs
|
|
27
|
+
|
|
28
|
+
- Created or modified documents in `knowledge/` or user-specified location
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## First: Ask About Edit Mode
|
|
33
|
+
|
|
34
|
+
**Before doing anything, ask:** "Should I make edits directly, or show you
|
|
35
|
+
changes first for approval?"
|
|
36
|
+
|
|
37
|
+
- **Direct mode:** Make edits immediately, confirm after
|
|
38
|
+
- **Approval mode:** Show proposed changes, wait for approval
|
|
39
|
+
|
|
40
|
+
Follow their choice for the entire session.
|
|
41
|
+
|
|
42
|
+
## Core Principles
|
|
43
|
+
|
|
44
|
+
- **Re-read before every response** — the user may have edited the file manually
|
|
45
|
+
- **Be concise** — don't propose outlines unless asked
|
|
46
|
+
- **Don't assume** — if unclear, ask ONE simple question
|
|
47
|
+
- **Use knowledge context** — search knowledge base for mentioned entities, use
|
|
48
|
+
`[[wiki-links]]`
|
|
49
|
+
|
|
50
|
+
## Processing Flow
|
|
51
|
+
|
|
52
|
+
### Step 1: Find the Document
|
|
53
|
+
|
|
54
|
+
Search thoroughly before saying a document doesn't exist:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
rg -l -i "roadmap" knowledge/
|
|
58
|
+
find knowledge/ -iname "*roadmap*" 2>/dev/null
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**If found:** Read it and proceed. **If NOT found:** Ask "I couldn't find
|
|
62
|
+
[name]. Shall I create it?"
|
|
63
|
+
|
|
64
|
+
**Creating new documents:**
|
|
65
|
+
|
|
66
|
+
1. Ask: "Shall I create knowledge/[name].md?"
|
|
67
|
+
2. Create with just a title — don't pre-populate with structure
|
|
68
|
+
3. Ask: "What would you like in this?"
|
|
69
|
+
|
|
70
|
+
### Step 2: Understand the Request
|
|
71
|
+
|
|
72
|
+
**NEVER make unsolicited edits.** If the user hasn't specified what to change,
|
|
73
|
+
ask: "What would you like to change?"
|
|
74
|
+
|
|
75
|
+
Types of requests:
|
|
76
|
+
|
|
77
|
+
1. **Direct edits** — "Change the title", "Add a bullet about Y"
|
|
78
|
+
2. **Content generation** — "Write an intro", "Draft the summary"
|
|
79
|
+
3. **Review/feedback** — "What do you think?", "Is this clear?"
|
|
80
|
+
4. **Research-backed additions** — "Add context about [Person]"
|
|
81
|
+
5. **No clear request** — Read the doc, then ask: "What would you like to
|
|
82
|
+
change?"
|
|
83
|
+
|
|
84
|
+
### Step 3: Execute Changes
|
|
85
|
+
|
|
86
|
+
Make targeted edits — change only what's needed. Preserve the user's voice and
|
|
87
|
+
don't reorganize unless asked.
|
|
88
|
+
|
|
89
|
+
### Step 4: Confirm and Continue
|
|
90
|
+
|
|
91
|
+
- Briefly confirm: "Added the executive summary section"
|
|
92
|
+
- Ask: "What's next?"
|
|
93
|
+
- Don't read back the entire document unless asked
|
|
94
|
+
|
|
95
|
+
## Searching Knowledge for Context
|
|
96
|
+
|
|
97
|
+
When the user mentions people, companies, or projects:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
rg -l "Name" knowledge/
|
|
101
|
+
cat "knowledge/People/Person.md"
|
|
102
|
+
cat "knowledge/Organizations/Company.md"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use `[[wiki-links]]` to connect to other notes. Only link to notes that exist.
|
|
106
|
+
|
|
107
|
+
## Constraints
|
|
108
|
+
|
|
109
|
+
- Match the user's tone and style
|
|
110
|
+
- Make surgical edits — change only what's needed
|
|
111
|
+
- Preserve the user's voice — don't reorganize unless asked
|
|
112
|
+
- Only link to notes that exist — use `[[Person Name]]` for existing notes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doc-create
|
|
3
|
+
description: Generate PDF documents from user requests using Playwright to render HTML to A4 PDF. Use when the user asks to create a document, proposal, report, or any multi-page PDF that is not a slide deck. Pulls context from the knowledge base for company info, project details, and people.
|
|
4
|
+
compatibility: Requires Node.js installed. Playwright is installed on first use.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Create Documents
|
|
8
|
+
|
|
9
|
+
Generate multi-page A4 PDF documents from user requests. Uses Playwright to
|
|
10
|
+
render self-contained HTML to PDF. Can pull context from the knowledge base for
|
|
11
|
+
company info, project details, and people.
|
|
12
|
+
|
|
13
|
+
## Trigger
|
|
14
|
+
|
|
15
|
+
Run when the user asks to create a document, proposal, report, funding
|
|
16
|
+
submission, brief, or any multi-page PDF that is not a slide deck.
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
- Node.js installed
|
|
21
|
+
- Playwright will be installed on first use
|
|
22
|
+
|
|
23
|
+
## Inputs
|
|
24
|
+
|
|
25
|
+
- User's description of the document
|
|
26
|
+
- `knowledge/` — optional context about company, product, team, projects
|
|
27
|
+
|
|
28
|
+
## Outputs
|
|
29
|
+
|
|
30
|
+
- An HTML file and a PDF rendered from it, placed where the user specifies
|
|
31
|
+
(default: `knowledge/Projects/`)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Workflow
|
|
36
|
+
|
|
37
|
+
1. Check `knowledge/` for relevant context about the company, product, team,
|
|
38
|
+
projects, or people mentioned.
|
|
39
|
+
2. Ensure Playwright is installed:
|
|
40
|
+
`bun install playwright && bunx playwright install chromium`
|
|
41
|
+
3. Create a self-contained HTML file with all CSS inlined. The HTML must handle
|
|
42
|
+
its own page layout — see **HTML Document Rules** below.
|
|
43
|
+
4. Run the conversion script:
|
|
44
|
+
|
|
45
|
+
node .claude/skills/doc-create/scripts/convert-to-pdf.mjs <input.html> [output.pdf]
|
|
46
|
+
|
|
47
|
+
If output is omitted, the PDF is written alongside the HTML file with the
|
|
48
|
+
same name.
|
|
49
|
+
|
|
50
|
+
5. Read the PDF back to visually verify it renders correctly. Check each page
|
|
51
|
+
for overflow, clipped content, and correct page breaks. Fix and re-render if
|
|
52
|
+
needed.
|
|
53
|
+
|
|
54
|
+
**Do NOT show HTML code to the user. Just create the PDF and deliver it.**
|
|
55
|
+
|
|
56
|
+
## HTML Document Rules
|
|
57
|
+
|
|
58
|
+
**Page layout:**
|
|
59
|
+
|
|
60
|
+
- Each page is a `<div class="page">` sized to exactly 210mm × 297mm (A4)
|
|
61
|
+
- Use `page-break-after: always` on every `.page` except the last
|
|
62
|
+
- Handle margins with padding inside `.page`, not with PDF margin settings
|
|
63
|
+
- The PDF is rendered with zero margins — the HTML owns all spacing
|
|
64
|
+
|
|
65
|
+
**Print colours:**
|
|
66
|
+
|
|
67
|
+
- Always set `-webkit-print-color-adjust: exact` and `print-color-adjust: exact`
|
|
68
|
+
on `body` so background colours render in the PDF
|
|
69
|
+
|
|
70
|
+
**Fonts:**
|
|
71
|
+
|
|
72
|
+
- Use system fonts only — no external font loading
|
|
73
|
+
- Monospace stack: `'SF Mono', 'Menlo', 'Monaco', 'Consolas', monospace`
|
|
74
|
+
- Sans-serif stack:
|
|
75
|
+
`-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif`
|
|
76
|
+
|
|
77
|
+
**Content fitting:**
|
|
78
|
+
|
|
79
|
+
- After the first render, visually check every page for overflow
|
|
80
|
+
- Content must not bleed past the `.page` boundary — if it does, reduce spacing
|
|
81
|
+
or font sizes and re-render
|
|
82
|
+
- Page numbers, if used, must not overlap with content. Position them in a
|
|
83
|
+
corner that has whitespace
|
|
84
|
+
|
|
85
|
+
**CSS @page:**
|
|
86
|
+
|
|
87
|
+
```css
|
|
88
|
+
@page { size: A4; margin: 0; }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Images:**
|
|
92
|
+
|
|
93
|
+
- Use absolute `file://` paths for local images
|
|
94
|
+
- Inline small images as base64 data URIs when possible
|
|
95
|
+
- Verify images appear in the rendered PDF — Playwright can fail silently on
|
|
96
|
+
missing images
|
|
97
|
+
|
|
98
|
+
## Design Principles
|
|
99
|
+
|
|
100
|
+
- Clean, professional typography with clear hierarchy
|
|
101
|
+
- Use monospace for section headers and numbers for a technical/engineering feel
|
|
102
|
+
- Tables should be compact and readable — right-align monetary values
|
|
103
|
+
- Use colour sparingly: one accent colour, one dark, lots of white space
|
|
104
|
+
- Dark-background panels (timelines, hero sections) create visual contrast
|
|
105
|
+
- Callout boxes with left borders draw attention to key statements
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Convert an HTML document to A4 PDF using Playwright.
|
|
4
|
+
*
|
|
5
|
+
* Renders an HTML file into an A4 PDF with background colours preserved and
|
|
6
|
+
* zero margins (the HTML is expected to handle its own page layout via CSS
|
|
7
|
+
* @page rules and page-break-after). Defaults to writing the PDF alongside
|
|
8
|
+
* the input file when no output path is given.
|
|
9
|
+
*
|
|
10
|
+
* Requires: bun install playwright && bunx playwright install chromium
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { resolve, dirname, basename, join } from "node:path";
|
|
14
|
+
|
|
15
|
+
const HELP = `convert-to-pdf — render an HTML document to A4 PDF via Playwright
|
|
16
|
+
|
|
17
|
+
Usage: bun scripts/convert-to-pdf.mjs <input.html> [output.pdf] [-h|--help]
|
|
18
|
+
|
|
19
|
+
Arguments:
|
|
20
|
+
input.html HTML document file (required)
|
|
21
|
+
output.pdf Output PDF path (default: same directory and name as input, with .pdf extension)
|
|
22
|
+
|
|
23
|
+
Requires: bun install playwright && bunx playwright install chromium`;
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
process.argv.includes("-h") ||
|
|
27
|
+
process.argv.includes("--help") ||
|
|
28
|
+
process.argv.length < 3
|
|
29
|
+
) {
|
|
30
|
+
console.log(HELP);
|
|
31
|
+
process.exit(process.argv.length < 3 ? 1 : 0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const positional = process.argv.slice(2).filter((a) => !a.startsWith("-"));
|
|
35
|
+
const input = resolve(positional[0]);
|
|
36
|
+
const output =
|
|
37
|
+
positional[1] || join(dirname(input), basename(input, ".html") + ".pdf");
|
|
38
|
+
|
|
39
|
+
const { chromium } = await import("playwright");
|
|
40
|
+
const browser = await chromium.launch();
|
|
41
|
+
const page = await browser.newPage();
|
|
42
|
+
await page.goto(`file://${input}`, { waitUntil: "networkidle" });
|
|
43
|
+
await page.pdf({
|
|
44
|
+
path: output,
|
|
45
|
+
format: "A4",
|
|
46
|
+
printBackground: true,
|
|
47
|
+
margin: { top: "0", right: "0", bottom: "0", left: "0" },
|
|
48
|
+
});
|
|
49
|
+
await browser.close();
|
|
50
|
+
console.log(`Done: ${output}`);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: draft-emails
|
|
3
|
+
description: Draft and send email responses using the knowledge base and calendar for context. Use when the user asks to draft, reply to, respond to, or send an email.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Draft Emails
|
|
7
|
+
|
|
8
|
+
Draft and send email responses using the knowledge base and calendar for full
|
|
9
|
+
context on every person and conversation. Every draft requires explicit user
|
|
10
|
+
approval before sending.
|
|
11
|
+
|
|
12
|
+
## Trigger
|
|
13
|
+
|
|
14
|
+
The user asks to draft, reply to, respond to, or send an email.
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
- Knowledge base populated (from `extract-entities`).
|
|
19
|
+
- Synced email data in `~/.cache/fit/outpost/apple_mail/`.
|
|
20
|
+
|
|
21
|
+
## Data locations
|
|
22
|
+
|
|
23
|
+
| Data | Location |
|
|
24
|
+
| --------------- | -------------------------------------------- |
|
|
25
|
+
| People | `knowledge/People/*.md` |
|
|
26
|
+
| Organizations | `knowledge/Organizations/*.md` |
|
|
27
|
+
| Email threads | `~/.cache/fit/outpost/apple_mail/*.md` |
|
|
28
|
+
| Calendar events | `~/.cache/fit/outpost/apple_calendar/*.json` |
|
|
29
|
+
| Handled IDs | `drafts/handled` (one ID per line) |
|
|
30
|
+
| Ignored IDs | `drafts/ignored` (one ID per line) |
|
|
31
|
+
| Draft files | `drafts/{email_id}_draft.md` |
|
|
32
|
+
|
|
33
|
+
`handled` and `ignored` both exclude threads from `scan-emails.mjs`. Use
|
|
34
|
+
`handled` for resolved threads (sent here, replied manually, resolved via DM);
|
|
35
|
+
`ignored` for threads that need no response (newsletters, spam, outbound with no
|
|
36
|
+
reply).
|
|
37
|
+
|
|
38
|
+
<do_confirm_checklist goal="Verify a draft is safe and ready before sending">
|
|
39
|
+
|
|
40
|
+
- [ ] Sender and organization were looked up in `knowledge/` before drafting.
|
|
41
|
+
- [ ] Draft is a single email (not multiple variants) and matches the incoming
|
|
42
|
+
tone.
|
|
43
|
+
- [ ] Body has no sign-off / name / "Best" — Apple Mail signature handles it.
|
|
44
|
+
- [ ] Recruitment thread: candidate excluded from internal recipients; any
|
|
45
|
+
direct-to-candidate draft is flagged `⚠️ RECRUITER ONLY`.
|
|
46
|
+
- [ ] No sensitive personal data (health, politics, etc.) was included.
|
|
47
|
+
- [ ] User has explicitly approved the draft before any send.
|
|
48
|
+
- [ ] Send used `--draft <path>` so cleanup and `drafts/handled` happen
|
|
49
|
+
automatically.
|
|
50
|
+
|
|
51
|
+
</do_confirm_checklist>
|
|
52
|
+
|
|
53
|
+
## Procedure
|
|
54
|
+
|
|
55
|
+
### 1. Scan for new emails
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
node scripts/scan-emails.mjs
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Outputs `email_id<TAB>subject` for unprocessed emails (those not in
|
|
62
|
+
`drafts/handled` or `drafts/ignored`).
|
|
63
|
+
|
|
64
|
+
### 2. Classify
|
|
65
|
+
|
|
66
|
+
**Ignore** (append ID to `drafts/ignored`): newsletters, marketing, automated
|
|
67
|
+
notifications, spam, outbound with no reply.
|
|
68
|
+
|
|
69
|
+
**Draft a response**: meeting requests, personal mail from known contacts,
|
|
70
|
+
business inquiries or follow-ups, requests for information or action.
|
|
71
|
+
|
|
72
|
+
Be conservative with ignore — when in doubt, draft.
|
|
73
|
+
|
|
74
|
+
### 3. Gather context
|
|
75
|
+
|
|
76
|
+
Before drafting, look up the sender and organization in `knowledge/`:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
rg -l "sender_name" knowledge/
|
|
80
|
+
cat "knowledge/People/Sender Name.md"
|
|
81
|
+
cat "knowledge/Organizations/Company Name.md"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For scheduling emails, also read the relevant calendar event:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
ls ~/.cache/fit/outpost/apple_calendar/ 2>/dev/null
|
|
88
|
+
cat "$HOME/.cache/fit/outpost/apple_calendar/event123.json"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Extract role, organization, relationship history, and open items. If intent is
|
|
92
|
+
unclear or the person has multiple contexts, **ask** rather than guess.
|
|
93
|
+
|
|
94
|
+
### 4. Write the draft
|
|
95
|
+
|
|
96
|
+
Save to `drafts/{email_id}_draft.md` using the template in
|
|
97
|
+
[references/template.md](references/template.md). Reference past interactions
|
|
98
|
+
naturally; for scheduling, propose specific times from calendar availability.
|
|
99
|
+
|
|
100
|
+
### 5. Recruitment & staffing emails
|
|
101
|
+
|
|
102
|
+
Candidates **must never** be copied on internal threads about them.
|
|
103
|
+
|
|
104
|
+
- Identify the candidate from the thread and `knowledge/Candidates/`.
|
|
105
|
+
- Strip the candidate from To/CC; draft to internal stakeholders only.
|
|
106
|
+
- Direct-to-candidate emails carry the warning header
|
|
107
|
+
`⚠️ RECRUITER ONLY — This email goes directly to the candidate.`
|
|
108
|
+
|
|
109
|
+
If a thread mentions a candidate and includes multiple internal recipients,
|
|
110
|
+
treat it as internal and exclude the candidate.
|
|
111
|
+
|
|
112
|
+
### 6. Present and approve
|
|
113
|
+
|
|
114
|
+
Show the draft to the user. Wait for explicit approval before sending. Apply
|
|
115
|
+
edits and present again as needed.
|
|
116
|
+
|
|
117
|
+
### 7. Send
|
|
118
|
+
|
|
119
|
+
After approval, send via Apple Mail:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
node scripts/send-email.mjs \
|
|
123
|
+
--to "recipient@example.com" \
|
|
124
|
+
--cc "other@example.com" \
|
|
125
|
+
--subject "Re: Subject" \
|
|
126
|
+
--body "Plain text body" \
|
|
127
|
+
--draft "drafts/12345_draft.md"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Required: `--to`, `--subject`, `--body` (plain text). Optional: `--cc`, `--bcc`,
|
|
131
|
+
`--draft`. With `--draft`, the draft file is deleted and the email ID is
|
|
132
|
+
appended to `drafts/handled` automatically.
|
|
133
|
+
|
|
134
|
+
### 8. Mark handled without sending
|
|
135
|
+
|
|
136
|
+
When a thread is resolved through other channels:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
echo "$EMAIL_ID" >> drafts/handled
|
|
140
|
+
rm -f "drafts/${EMAIL_ID}_draft.md"
|
|
141
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Draft File Template
|
|
2
|
+
|
|
3
|
+
Save each draft to `drafts/{email_id}_draft.md` using this layout:
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
# Draft Response
|
|
7
|
+
|
|
8
|
+
**To:** recipient@example.com
|
|
9
|
+
**CC:** other@example.com
|
|
10
|
+
**Subject:** Re: {subject}
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{personalized draft body — no sign-off, no name at end}
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Notes
|
|
19
|
+
- **Original Email ID:** {id}
|
|
20
|
+
- **From:** {sender}
|
|
21
|
+
- **Context:** {knowledge base notes used}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The body must end with the last sentence of content. Apple Mail appends the
|
|
25
|
+
user's configured signature automatically, so do **not** add a sign-off, "Best",
|
|
26
|
+
"Cheers", or the user's name.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Scan for unprocessed emails and output their IDs and subjects.
|
|
4
|
+
*
|
|
5
|
+
* Checks ~/.cache/fit/outpost/apple_mail/ for email thread markdown files not
|
|
6
|
+
* yet listed in drafts/handled or drafts/ignored. Outputs one tab-separated
|
|
7
|
+
* line per unprocessed thread: email_id<TAB>subject. Used by the draft-emails
|
|
8
|
+
* skill to identify threads that need a reply.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { basename, join } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
|
|
15
|
+
const HELP = `scan-emails — list unprocessed email threads
|
|
16
|
+
|
|
17
|
+
Usage: node scripts/scan-emails.mjs [-h|--help]
|
|
18
|
+
|
|
19
|
+
Scans ~/.cache/fit/outpost/apple_mail/ for .md thread files not yet
|
|
20
|
+
recorded in drafts/handled or drafts/ignored. Outputs one line per
|
|
21
|
+
unprocessed thread as: email_id<TAB>subject`;
|
|
22
|
+
|
|
23
|
+
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
24
|
+
console.log(HELP);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const HOME = homedir();
|
|
29
|
+
const MAIL_DIR = join(HOME, ".cache/fit/outpost/apple_mail");
|
|
30
|
+
|
|
31
|
+
/** Load a file of IDs (one per line) into a Set. */
|
|
32
|
+
function loadIdSet(path) {
|
|
33
|
+
const ids = new Set();
|
|
34
|
+
if (!existsSync(path)) return ids;
|
|
35
|
+
for (const line of readFileSync(path, "utf-8").split("\n")) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (trimmed) ids.add(trimmed);
|
|
38
|
+
}
|
|
39
|
+
return ids;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Extract the first H1 heading from a markdown file. */
|
|
43
|
+
function extractSubject(filePath) {
|
|
44
|
+
const text = readFileSync(filePath, "utf-8");
|
|
45
|
+
const match = text.match(/^# (.+)$/m);
|
|
46
|
+
return match ? match[1] : "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function main() {
|
|
50
|
+
if (!existsSync(MAIL_DIR)) return;
|
|
51
|
+
|
|
52
|
+
const handled = loadIdSet("drafts/handled");
|
|
53
|
+
const ignored = loadIdSet("drafts/ignored");
|
|
54
|
+
|
|
55
|
+
for (const name of readdirSync(MAIL_DIR).sort()) {
|
|
56
|
+
if (!name.endsWith(".md")) continue;
|
|
57
|
+
|
|
58
|
+
const emailId = basename(name, ".md");
|
|
59
|
+
if (handled.has(emailId) || ignored.has(emailId)) continue;
|
|
60
|
+
|
|
61
|
+
const subject = extractSubject(join(MAIL_DIR, name));
|
|
62
|
+
console.log(`${emailId}\t${subject}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
main();
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Send an email via Apple Mail using AppleScript.
|
|
4
|
+
*
|
|
5
|
+
* Builds an AppleScript command to create and send an outgoing message through
|
|
6
|
+
* Apple Mail. The script writes a temporary .scpt file, executes it with
|
|
7
|
+
* osascript, and cleans up afterwards. Mail.app must be running.
|
|
8
|
+
*
|
|
9
|
+
* The body should be plain text — no HTML. Do NOT include an email signature
|
|
10
|
+
* or sign-off; Apple Mail appends the user's configured signature automatically.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import {
|
|
15
|
+
appendFileSync,
|
|
16
|
+
mkdtempSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
unlinkSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { basename, join } from "node:path";
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
|
|
23
|
+
const HELP = `send-email — send an email via Apple Mail
|
|
24
|
+
|
|
25
|
+
Usage: node scripts/send-email.mjs --to <addrs> --subject <subj> --body <text> [options]
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--to <addrs> Comma-separated To recipients (required)
|
|
29
|
+
--cc <addrs> Comma-separated CC recipients
|
|
30
|
+
--bcc <addrs> Comma-separated BCC recipients
|
|
31
|
+
--subject <subj> Email subject line (required)
|
|
32
|
+
--body <text> Plain-text email body (required)
|
|
33
|
+
--draft <path> Draft file — deleted after send, ID appended to drafts/handled
|
|
34
|
+
-h, --help Show this help message and exit
|
|
35
|
+
|
|
36
|
+
Mail.app must be running. No signature or sign-off needed — Apple Mail appends it.`;
|
|
37
|
+
|
|
38
|
+
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
39
|
+
console.log(HELP);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Escape a string for AppleScript double-quoted context. */
|
|
44
|
+
function escapeAS(s) {
|
|
45
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Build recipient lines for AppleScript. */
|
|
49
|
+
function recipientLines(type, addrs) {
|
|
50
|
+
if (!addrs) return "";
|
|
51
|
+
return addrs
|
|
52
|
+
.split(",")
|
|
53
|
+
.map((a) => a.trim())
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.map(
|
|
56
|
+
(addr) =>
|
|
57
|
+
` make new ${type} at end of ${type}s with properties {address:"${escapeAS(addr)}"}`,
|
|
58
|
+
)
|
|
59
|
+
.join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseArgs(args) {
|
|
63
|
+
const parsed = { to: "", cc: "", bcc: "", subject: "", body: "", draft: "" };
|
|
64
|
+
const keyMap = {
|
|
65
|
+
"--to": "to",
|
|
66
|
+
"--cc": "cc",
|
|
67
|
+
"--bcc": "bcc",
|
|
68
|
+
"--subject": "subject",
|
|
69
|
+
"--body": "body",
|
|
70
|
+
"--draft": "draft",
|
|
71
|
+
};
|
|
72
|
+
for (let i = 0; i < args.length; i++) {
|
|
73
|
+
const key = keyMap[args[i]];
|
|
74
|
+
if (key) parsed[key] = args[++i] ?? "";
|
|
75
|
+
}
|
|
76
|
+
if (!parsed.to || !parsed.subject || !parsed.body) {
|
|
77
|
+
console.error("Error: --to, --subject, and --body are required.");
|
|
78
|
+
console.error("Run with --help for usage info.");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
parsed.body = parsed.body
|
|
82
|
+
.split("\n")
|
|
83
|
+
.map((line) => line.replace(/^ {2}/, ""))
|
|
84
|
+
.join("\n")
|
|
85
|
+
.trim();
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildAppleScript({ to, cc, bcc, subject, body }) {
|
|
90
|
+
return [
|
|
91
|
+
'tell application "Mail"',
|
|
92
|
+
` set newMessage to make new outgoing message with properties {subject:"${escapeAS(subject)}", content:"${escapeAS(body)}", visible:false}`,
|
|
93
|
+
" tell newMessage",
|
|
94
|
+
recipientLines("to recipient", to),
|
|
95
|
+
cc ? recipientLines("cc recipient", cc) : "",
|
|
96
|
+
bcc ? recipientLines("bcc recipient", bcc) : "",
|
|
97
|
+
" end tell",
|
|
98
|
+
" send newMessage",
|
|
99
|
+
"end tell",
|
|
100
|
+
]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join("\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function handleDraftCleanup(draft) {
|
|
106
|
+
if (!draft) return;
|
|
107
|
+
try {
|
|
108
|
+
unlinkSync(draft);
|
|
109
|
+
console.log(`Removed draft: ${draft}`);
|
|
110
|
+
} catch {
|
|
111
|
+
/* ignore */
|
|
112
|
+
}
|
|
113
|
+
const draftBasename = basename(draft, ".md");
|
|
114
|
+
const emailId = draftBasename.replace(/_draft$/, "");
|
|
115
|
+
if (emailId) {
|
|
116
|
+
appendFileSync("drafts/handled", emailId + "\n");
|
|
117
|
+
console.log(`Marked as handled: ${emailId}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function main() {
|
|
122
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
123
|
+
const script = buildAppleScript(parsed);
|
|
124
|
+
const tmp = join(mkdtempSync(join(tmpdir(), "send-email-")), "mail.scpt");
|
|
125
|
+
try {
|
|
126
|
+
writeFileSync(tmp, script);
|
|
127
|
+
execFileSync("osascript", [tmp], { stdio: "inherit" });
|
|
128
|
+
console.log(`Sent: ${parsed.subject}`);
|
|
129
|
+
handleDraftCleanup(parsed.draft);
|
|
130
|
+
} finally {
|
|
131
|
+
try {
|
|
132
|
+
unlinkSync(tmp);
|
|
133
|
+
} catch {
|
|
134
|
+
/* ignore */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
main();
|