@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.
Files changed (120) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +15 -0
  3. package/bin/fit-outpost.js +3 -0
  4. package/config/scheduler.json +34 -0
  5. package/package.json +63 -0
  6. package/src/agent-runner.js +189 -0
  7. package/src/index.js +5 -0
  8. package/src/kb-manager.js +228 -0
  9. package/src/outpost.js +417 -0
  10. package/src/scheduler.js +195 -0
  11. package/src/socket-server.js +336 -0
  12. package/src/state-manager.js +126 -0
  13. package/templates/.claude/agents/chief-of-staff.md +58 -0
  14. package/templates/.claude/agents/concierge.md +46 -0
  15. package/templates/.claude/agents/head-hunter.md +57 -0
  16. package/templates/.claude/agents/librarian.md +60 -0
  17. package/templates/.claude/agents/postman.md +47 -0
  18. package/templates/.claude/agents/recruiter.md +63 -0
  19. package/templates/.claude/settings.json +88 -0
  20. package/templates/.claude/skills/candidate-report/SKILL.md +139 -0
  21. package/templates/.claude/skills/candidate-report/references/report-template.html +203 -0
  22. package/templates/.claude/skills/candidate-report/references/report.css +438 -0
  23. package/templates/.claude/skills/candidate-report/references/rubric.md +76 -0
  24. package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +48 -0
  25. package/templates/.claude/skills/deck-create/SKILL.md +75 -0
  26. package/templates/.claude/skills/deck-create/references/slide.css +35 -0
  27. package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +47 -0
  28. package/templates/.claude/skills/deck-summarize/SKILL.md +139 -0
  29. package/templates/.claude/skills/deck-summarize/references/brief-template.md +85 -0
  30. package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +210 -0
  31. package/templates/.claude/skills/doc-collab/SKILL.md +112 -0
  32. package/templates/.claude/skills/doc-create/SKILL.md +105 -0
  33. package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +50 -0
  34. package/templates/.claude/skills/draft-emails/SKILL.md +141 -0
  35. package/templates/.claude/skills/draft-emails/references/template.md +26 -0
  36. package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +66 -0
  37. package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +139 -0
  38. package/templates/.claude/skills/extract-entities/SKILL.md +187 -0
  39. package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +14 -0
  40. package/templates/.claude/skills/extract-entities/references/conditions.md +55 -0
  41. package/templates/.claude/skills/extract-entities/references/content.md +104 -0
  42. package/templates/.claude/skills/extract-entities/references/links.md +48 -0
  43. package/templates/.claude/skills/extract-entities/references/recruitment.md +57 -0
  44. package/templates/.claude/skills/extract-entities/references/resolution.md +71 -0
  45. package/templates/.claude/skills/extract-entities/references/sources.md +70 -0
  46. package/templates/.claude/skills/extract-entities/references/templates-conditions.md +36 -0
  47. package/templates/.claude/skills/extract-entities/references/templates-goals-priorities.md +63 -0
  48. package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +67 -0
  49. package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +68 -0
  50. package/templates/.claude/skills/extract-entities/scripts/state.mjs +131 -0
  51. package/templates/.claude/skills/hyprnote-follow/SKILL.md +174 -0
  52. package/templates/.claude/skills/hyprnote-follow/references/coaching.md +118 -0
  53. package/templates/.claude/skills/hyprnote-follow/references/debrief.md +59 -0
  54. package/templates/.claude/skills/hyprnote-follow/references/meeting-types.md +14 -0
  55. package/templates/.claude/skills/hyprnote-follow/scripts/follow.mjs +243 -0
  56. package/templates/.claude/skills/hyprnote-process/SKILL.md +147 -0
  57. package/templates/.claude/skills/hyprnote-process/references/extraction.md +84 -0
  58. package/templates/.claude/skills/hyprnote-process/references/sessions.md +81 -0
  59. package/templates/.claude/skills/hyprnote-process/scripts/scan.mjs +246 -0
  60. package/templates/.claude/skills/hyprnote-trim/SKILL.md +177 -0
  61. package/templates/.claude/skills/manage-tasks/SKILL.md +152 -0
  62. package/templates/.claude/skills/manage-tasks/references/format.md +73 -0
  63. package/templates/.claude/skills/meeting-prep/SKILL.md +180 -0
  64. package/templates/.claude/skills/organize-files/SKILL.md +146 -0
  65. package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +105 -0
  66. package/templates/.claude/skills/organize-files/scripts/summarize.mjs +91 -0
  67. package/templates/.claude/skills/req-assess/SKILL.md +137 -0
  68. package/templates/.claude/skills/req-assess/references/interview-template.md +62 -0
  69. package/templates/.claude/skills/req-assess/references/panel-template.md +117 -0
  70. package/templates/.claude/skills/req-assess/references/rubric.md +65 -0
  71. package/templates/.claude/skills/req-decide/SKILL.md +163 -0
  72. package/templates/.claude/skills/req-decide/references/rubric.md +63 -0
  73. package/templates/.claude/skills/req-decide/references/template.md +118 -0
  74. package/templates/.claude/skills/req-forget/SKILL.md +154 -0
  75. package/templates/.claude/skills/req-forget/references/classify.md +23 -0
  76. package/templates/.claude/skills/req-forget/references/locations.md +59 -0
  77. package/templates/.claude/skills/req-forget/references/report-template.md +67 -0
  78. package/templates/.claude/skills/req-scan/SKILL.md +145 -0
  79. package/templates/.claude/skills/req-scan/references/fallbacks.md +76 -0
  80. package/templates/.claude/skills/req-scan/references/filters.md +83 -0
  81. package/templates/.claude/skills/req-scan/references/sources.md +76 -0
  82. package/templates/.claude/skills/req-scan/references/state.md +54 -0
  83. package/templates/.claude/skills/req-scan/references/template.md +36 -0
  84. package/templates/.claude/skills/req-scan/scripts/state.mjs +396 -0
  85. package/templates/.claude/skills/req-screen/SKILL.md +151 -0
  86. package/templates/.claude/skills/req-screen/references/rubric.md +94 -0
  87. package/templates/.claude/skills/req-screen/references/template.md +91 -0
  88. package/templates/.claude/skills/req-track/SKILL.md +189 -0
  89. package/templates/.claude/skills/req-track/references/fields.md +114 -0
  90. package/templates/.claude/skills/req-track/references/signals.md +42 -0
  91. package/templates/.claude/skills/req-track/references/statuses.md +32 -0
  92. package/templates/.claude/skills/req-track/references/templates.md +127 -0
  93. package/templates/.claude/skills/req-workday/SKILL.md +162 -0
  94. package/templates/.claude/skills/req-workday/references/field-mapping.md +40 -0
  95. package/templates/.claude/skills/req-workday/references/status-mapping.md +38 -0
  96. package/templates/.claude/skills/req-workday/references/templates.md +126 -0
  97. package/templates/.claude/skills/req-workday/references/xlsx-format.md +56 -0
  98. package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +313 -0
  99. package/templates/.claude/skills/send-chat/SKILL.md +170 -0
  100. package/templates/.claude/skills/sync-apple-calendar/SKILL.md +145 -0
  101. package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +80 -0
  102. package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +307 -0
  103. package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +332 -0
  104. package/templates/.claude/skills/sync-apple-mail/SKILL.md +151 -0
  105. package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +115 -0
  106. package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +389 -0
  107. package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +372 -0
  108. package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +239 -0
  109. package/templates/.claude/skills/sync-teams/SKILL.md +177 -0
  110. package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +147 -0
  111. package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +288 -0
  112. package/templates/.claude/skills/sync-teams/scripts/sync.mjs +381 -0
  113. package/templates/.claude/skills/upstream-skill/SKILL.md +130 -0
  114. package/templates/.claude/skills/upstream-skill/references/examples.md +80 -0
  115. package/templates/.claude/skills/weekly-update/SKILL.md +141 -0
  116. package/templates/.claude/skills/weekly-update/references/template.md +55 -0
  117. package/templates/CLAUDE.md +134 -0
  118. package/templates/USER.md +3 -0
  119. package/templates/apm.yml +5 -0
  120. 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();