@forwardimpact/outpost 3.8.1 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/config/scheduler.json +12 -6
  2. package/package.json +1 -1
  3. package/src/agent-runner.js +27 -0
  4. package/src/kb-manager.js +54 -1
  5. package/src/outpost.js +12 -5
  6. package/src/privilege.js +45 -0
  7. package/templates/.claude/agents/chief-of-staff.md +9 -7
  8. package/templates/.claude/agents/concierge.md +6 -4
  9. package/templates/.claude/agents/head-hunter.md +6 -4
  10. package/templates/.claude/agents/librarian.md +14 -10
  11. package/templates/.claude/agents/postman.md +6 -4
  12. package/templates/.claude/agents/recruiter.md +8 -5
  13. package/templates/.claude/skills/anarlog-follow/references/coaching.md +3 -3
  14. package/templates/.claude/skills/anarlog-trim/SKILL.md +9 -3
  15. package/templates/.claude/skills/candidate-report/SKILL.md +4 -3
  16. package/templates/.claude/skills/changelog/SKILL.md +10 -10
  17. package/templates/.claude/skills/deck-create/SKILL.md +23 -22
  18. package/templates/.claude/skills/deck-review/SKILL.md +29 -25
  19. package/templates/.claude/skills/doc-create/SKILL.md +17 -16
  20. package/templates/.claude/skills/extract-entities/SKILL.md +4 -2
  21. package/templates/.claude/skills/meeting-prep/SKILL.md +2 -1
  22. package/templates/.claude/skills/organize-files/SKILL.md +9 -5
  23. package/templates/.claude/skills/person-identify/SKILL.md +2 -2
  24. package/templates/.claude/skills/person-lookup/SKILL.md +3 -3
  25. package/templates/.claude/skills/req-forget/references/report-template.md +1 -1
  26. package/templates/.claude/skills/req-scan/references/fallbacks.md +3 -3
  27. package/templates/.claude/skills/req-scan/references/sources.md +5 -5
  28. package/templates/.claude/skills/req-track/SKILL.md +2 -2
  29. package/templates/.claude/skills/req-workday/references/status-mapping.md +1 -1
  30. package/templates/.claude/skills/sync-apple-calendar/SKILL.md +6 -2
  31. package/templates/.claude/skills/sync-apple-mail/SKILL.md +3 -1
  32. package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +1 -1
  33. package/templates/.claude/skills/sync-teams/SKILL.md +13 -1
  34. package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +67 -34
  35. package/templates/.claude/skills/upstream-instructions/SKILL.md +2 -1
  36. package/templates/CLAUDE.md +27 -38
@@ -32,18 +32,18 @@ Run when the user asks to create a presentation, slide deck, or pitch deck.
32
32
 
33
33
  ## Workflow
34
34
 
35
- 1. Check `Knowledge/` for relevant context about the company, product, team,
36
- etc.
37
- 2. Ensure Playwright is installed:
38
- `bun install playwright && bunx playwright install chromium`
39
- 3. Create an HTML file at `/tmp/outpost-presentation.html` with slides
40
- (1280x720px each)
41
- 4. Include the required CSS from [references/slide.css](references/slide.css)
42
- 5. Run the conversion script:
35
+ 1. Check `Knowledge/` for relevant context about the company, product, team,
36
+ etc.
37
+ 2. Ensure Playwright is installed:
38
+ `bun install playwright && bunx playwright install chromium`
39
+ 3. Create an HTML file at `/tmp/outpost-presentation.html` with slides
40
+ (1280x720px each)
41
+ 4. Include the required CSS from [references/slide.css](references/slide.css)
42
+ 5. Run the conversion script:
43
43
 
44
44
  node scripts/convert-to-pdf.mjs
45
45
 
46
- 6. Tell the user: "Your presentation is ready at ~/Desktop/presentation.pdf"
46
+ 6. Tell the user: "Your presentation is ready at ~/Desktop/presentation.pdf"
47
47
 
48
48
  **Do NOT show HTML code to the user. Just create the PDF and deliver it.**
49
49
 
@@ -82,28 +82,29 @@ selecting/copying text on a slide, and typing into overlay tools (e.g. the
82
82
  2. **No click-to-advance.** Do NOT add click regions on the slide/stage that
83
83
  navigate (e.g. "click left/right third"). They fire on the mouse-up that ends
84
84
  a text-selection drag and jump the slide unexpectedly.
85
- 3. **No spacebar, PageUp/PageDown, or other global key bindings.** Space conflicts
86
- with typing in overlay inputs; the rest are redundant and surprising.
87
- 4. **A progress indicator may be clickable**, but it must live in the footer/chrome
88
- and never overlap slide content.
85
+ 3. **No spacebar, PageUp/PageDown, or other global key bindings.** Space
86
+ conflicts with typing in overlay inputs; the rest are redundant and
87
+ surprising.
88
+ 4. **A progress indicator may be clickable**, but it must live in the
89
+ footer/chrome and never overlap slide content.
89
90
  5. **Expose `window.deckGoto(index)`** (0-based) right after the slide-show
90
- function, so review/overlay tools can jump to a slide without simulating clicks
91
- or keys:
91
+ function, so review/overlay tools can jump to a slide without simulating
92
+ clicks or keys:
92
93
 
93
94
  function go(n) { /* ...show slide n... */ }
94
95
  window.deckGoto = go;
95
96
 
96
- 6. **Keep the hint honest** — the on-screen nav hint should read `← → to navigate`
97
- (don't advertise click/space).
97
+ 6. **Keep the hint honest** — the on-screen nav hint should read
98
+ `← → to navigate` (don't advertise click/space).
98
99
  7. **Use stable structural hooks.** Make each slide one element with class
99
100
  `.slide`, and put the slide-number label (if any) in a `.slide-num` element.
100
101
  The review overlay defaults to these selectors to detect and index slides.
101
102
 
102
- These rules keep decks compatible with the **`deck-review`** skill, which installs
103
- the `slide-annotator.js` review overlay (highlight text on a slide → sidecar JSON
104
- of feedback that an agent acts on). After producing an interactive HTML deck, you
105
- can offer to run `deck-review` to make it reviewable; see that skill for the
106
- install steps and the sidecar JSON schema.
103
+ These rules keep decks compatible with the **`deck-review`** skill, which
104
+ installs the `slide-annotator.js` review overlay (highlight text on a slide →
105
+ sidecar JSON of feedback that an agent acts on). After producing an interactive
106
+ HTML deck, you can offer to run `deck-review` to make it reviewable; see that
107
+ skill for the install steps and the sidecar JSON schema.
107
108
 
108
109
  ## Constraints
109
110
 
@@ -8,9 +8,10 @@ compatibility: Standalone HTML deck opened in a Chromium-based browser (Chrome/E
8
8
 
9
9
  Install the self-contained `slide-annotator.js` overlay onto an HTML deck so the
10
10
  user can **highlight text on a slide and save the feedback as a sidecar JSON**.
11
- Each annotation carries a robust anchor (exact text + surrounding context + slide)
12
- and, once the folder is connected, the resolved **source line, column and context
13
- lines** — so an agent can locate and edit the exact text in small iterations.
11
+ Each annotation carries a robust anchor (exact text + surrounding context +
12
+ slide) and, once the folder is connected, the resolved
13
+ **source line, column and context lines** — so an agent can locate and edit the
14
+ exact text in small iterations.
14
15
 
15
16
  This is the companion to **`deck-create`**: decks produced by `deck-create`
16
17
  already follow the navigation/structure standards this overlay needs, and this
@@ -19,8 +20,8 @@ overlay is designed to drop onto them with one script tag.
19
20
  ## Trigger
20
21
 
21
22
  Run when the user asks to add review / annotation / highlight / comment / markup
22
- capability to a deck, "make this deck reviewable", or to set up a feedback loop on
23
- slides.
23
+ capability to a deck, "make this deck reviewable", or to set up a feedback loop
24
+ on slides.
24
25
 
25
26
  ## Inputs
26
27
 
@@ -46,10 +47,10 @@ slides.
46
47
  - **Slide selector** — each slide is one element with a stable class
47
48
  (default `.slide`). If the deck uses a different class, note it for step 4.
48
49
  - **Navigation hook** — the deck exposes `window.deckGoto(index)` (0-based).
49
- If it has a slideshow function (e.g. `go(n)`) but no hook, add one line right
50
- after it: `window.deckGoto = go;`. Without it the overlay still works (the
51
- panel's *Go* button falls back to `scrollIntoView`), but it can't jump to a
52
- hidden slide precisely.
50
+ If it has a slideshow function (e.g. `go(n)`) but no hook, add one line
51
+ right after it: `window.deckGoto = go;`. Without it the overlay still works
52
+ (the panel's *Go* button falls back to `scrollIntoView`), but it can't jump
53
+ to a hidden slide precisely.
53
54
  - Optionally a slide-number label element (default `.slide-num`) for nicer
54
55
  labels in the panel — purely cosmetic.
55
56
 
@@ -57,8 +58,8 @@ slides.
57
58
  **same directory as the deck**. Resolve `~` to `$HOME`; pass the Write/copy a
58
59
  full path.
59
60
 
60
- 4. **Inject the script tag** immediately before `</body>` (idempotent — skip if a
61
- `slide-annotator` script tag is already present):
61
+ 4. **Inject the script tag** immediately before `</body>` (idempotent — skip if
62
+ a `slide-annotator` script tag is already present):
62
63
 
63
64
  ```html
64
65
  <!-- Review overlay: highlight text on a slide → sidecar JSON. Self-contained, optional. -->
@@ -86,21 +87,23 @@ follow:
86
87
  | Navigation hook | `window.deckGoto(index)` (0-based) | panel "Go" jumps to the right slide |
87
88
  | Arrow-keys-only navigation, **no** click-to-advance / spacebar | — | text selection + typing in the overlay must not move slides |
88
89
 
89
- If a deck violates the last row (has click-to-advance), the overlay's click guard
90
- only suppresses the click that ends a text-selection drag, so it degrades
90
+ If a deck violates the last row (has click-to-advance), the overlay's click
91
+ guard only suppresses the click that ends a text-selection drag, so it degrades
91
92
  gracefully — but the correct fix is to make the deck arrow-keys-only per
92
93
  `deck-create`'s *Navigation & Event Standards*.
93
94
 
94
95
  ## Using the overlay (tell the user)
95
96
 
96
97
  1. Open the deck in Chrome and click **✎ Review** (bottom-left).
97
- 2. **Select text** on a slide → a popover lets you add an optional note → **Add**.
98
- The highlight appears and **autosaves to `localStorage`** immediately.
99
- 3. Click **Connect folder** once and pick the deck's folder. From then on **Save**
100
- writes a real `‹deck›.annotations.json` next to the deck, and the tool reads the
101
- deck's own source to fill in **source line / column / context** for each
102
- highlight. (If the browser blocks folder access on `file://`, **Save**
103
- downloads the JSON instead move it next to the deck.)
98
+ 2. **Select text** on a slide → a popover lets you add an optional note →
99
+ **Add**. The highlight appears and **autosaves to `localStorage`**
100
+ immediately.
101
+ 3. Click **Connect folder** once and pick the deck's folder. From then on
102
+ **Save** writes a real `‹deck›.annotations.json` next to the deck, and the
103
+ tool reads the deck's own source to fill in
104
+ **source line / column / context** for each highlight. (If the browser blocks
105
+ folder access on `file://`, **Save** downloads the JSON instead — move it
106
+ next to the deck.)
104
107
  4. Navigation while reviewing is the deck's normal **← / →** (the overlay's own
105
108
  keystrokes never leak to the deck).
106
109
 
@@ -143,14 +146,15 @@ When the user says "work the annotations":
143
146
 
144
147
  ## Removing the overlay (for final delivery)
145
148
 
146
- To hand off a clean presentation, delete the injected `<script src="slide-annotator.js" …>`
147
- line and the `slide-annotator.js` file. Leaving the `window.deckGoto = go;` line in
148
- the deck is harmless.
149
+ To hand off a clean presentation, delete the injected
150
+ `<script src="slide-annotator.js" …>` line and the `slide-annotator.js` file.
151
+ Leaving the `window.deckGoto = go;` line in the deck is harmless.
149
152
 
150
153
  ## Constraints
151
154
 
152
155
  - Keep `slide-annotator.js` **dependency-free and host-agnostic** — it must work
153
156
  on any static HTML page, not just `deck-create` output.
154
- - Edit the tool **here** (`assets/slide-annotator.js`) as the source of truth, then
155
- re-install onto decks. Don't fork per-deck copies with divergent behavior.
157
+ - Edit the tool **here** (`assets/slide-annotator.js`) as the source of truth,
158
+ then re-install onto decks. Don't fork per-deck copies with divergent
159
+ behavior.
156
160
  - Never auto-send or upload annotations anywhere — the sidecar JSON stays local.
@@ -34,22 +34,23 @@ submission, brief, or any multi-page PDF that is not a slide deck.
34
34
 
35
35
  ## Workflow
36
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.
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
+ ```text
46
+ node .claude/skills/doc-create/scripts/convert-to-pdf.mjs <input.html> [output.pdf]
47
+ ```
48
+
49
+ If output is omitted, the PDF is written alongside the HTML file with the
50
+ same name.
51
+ 5. Read the PDF back to visually verify it renders correctly. Check each page
52
+ for overflow, clipped content, and correct page breaks. Fix and re-render if
53
+ needed.
53
54
 
54
55
  **Do NOT show HTML code to the user. Just create the PDF and deliver it.**
55
56
 
@@ -76,7 +76,8 @@ writes.
76
76
  ### 0. Load context and pick the batch
77
77
 
78
78
  Read the user's identity from `~/.cache/fit/outpost/state/identity.md` (run the
79
- `person-identify` skill first if it is missing or stale). Find new/changed files:
79
+ `person-identify` skill first if it is missing or stale). Find new/changed
80
+ files:
80
81
 
81
82
  ```bash
82
83
  node scripts/state.mjs check
@@ -151,7 +152,8 @@ filler or meta-commentary.
151
152
  domain-lead inference):
152
153
  [references/recruitment.md](references/recruitment.md).
153
154
  - **Priority links** (Step 7c): rules in
154
- [references/links.md](references/links.md#priorities-step-7c). **Never auto-create.**
155
+ [references/links.md](references/links.md#priorities-step-7c).
156
+ **Never auto-create.**
155
157
  - **Conditions** (cross-cutting states affecting ≥ 3 entities):
156
158
  [references/conditions.md](references/conditions.md).
157
159
 
@@ -25,7 +25,8 @@ meetings.
25
25
  - `Knowledge/People/*.md` — attendee context
26
26
  - `Knowledge/Organizations/*.md` — company context
27
27
  - `Knowledge/Projects/*.md` — project context
28
- - `Knowledge/Priorities/*.md` — active priorities and strategic context for framing
28
+ - `Knowledge/Priorities/*.md` — active priorities and strategic context for
29
+ framing
29
30
  - `Knowledge/Candidates/*/brief.md` — candidate context (for interview meetings)
30
31
  - `Knowledge/Roles/*.md` — role/requisition context (for interview meetings)
31
32
 
@@ -61,7 +61,9 @@ Run when the user asks to find, organize, clean up, or tidy files on their Mac.
61
61
 
62
62
  Get an overview of both directories:
63
63
 
64
- node scripts/summarize.mjs
64
+ ```text
65
+ node scripts/summarize.mjs
66
+ ```
65
67
 
66
68
  ## Finding Files
67
69
 
@@ -79,8 +81,10 @@ find ~/Desktop -maxdepth 1 \( -name "Screenshot*" -o -name "Screen Shot*" \)
79
81
  Organize a directory into type-based subdirectories (Documents, Images,
80
82
  Archives, Installers, Screenshots):
81
83
 
82
- node scripts/organize-by-type.mjs ~/Downloads
83
- node scripts/organize-by-type.mjs ~/Desktop
84
+ ```text
85
+ node scripts/organize-by-type.mjs ~/Downloads
86
+ node scripts/organize-by-type.mjs ~/Desktop
87
+ ```
84
88
 
85
89
  The script creates subdirectories and moves matching files. It does NOT delete
86
90
  anything.
@@ -106,7 +110,7 @@ After organizing, collect the paths of document files and invoke the
106
110
 
107
111
  **Plan:**
108
112
 
109
- ```
113
+ ```text
110
114
  Organization Plan: Desktop & Downloads Cleanup
111
115
 
112
116
  Found 47 files to organize:
@@ -123,7 +127,7 @@ Should I proceed?
123
127
 
124
128
  **Results:**
125
129
 
126
- ```
130
+ ```text
127
131
  Organization Complete
128
132
 
129
133
  Moved 47 files:
@@ -98,7 +98,7 @@ Key attributes returned (names per Active Directory schema):
98
98
 
99
99
  - To look up **someone else**, use the sibling `person-lookup` skill — it takes
100
100
  free-text input (email or name), searches the Global Catalog forest-wide
101
- (`ldap://$dc:3268 -b ''`), handles multiple matches, and does **not** touch the
102
- identity cache.
101
+ (`ldap://$dc:3268 -b ''`), handles multiple matches, and does **not** touch
102
+ the identity cache.
103
103
  - Not Active Directory? The same `ldapsearch -Y GSSAPI` shape works against any
104
104
  Kerberos-backed LDAP directory; only the attribute names differ.
@@ -86,9 +86,9 @@ The argument is free text: an email, a full name, or just a surname.
86
86
  since the OU convention is organization-specific. Narrow with an email for an
87
87
  exact hit.
88
88
  - **Silent partial results.** Under load the directory occasionally returns an
89
- entry's DN with no attributes (exit 0, no error). Every attribute fetch retries
90
- with backoff, so a throttled response never masquerades as a person with a
91
- blank title or email.
89
+ entry's DN with no attributes (exit 0, no error). Every attribute fetch
90
+ retries with backoff, so a throttled response never masquerades as a person
91
+ with a blank title or email.
92
92
  - **No cache.** This skill prints and exits. It never touches
93
93
  `~/.cache/fit/outpost/state/identity.md` — that file is owned solely by
94
94
  `person-identify`.
@@ -62,6 +62,6 @@ rg "{Name}" Knowledge/ ~/.cache/fit/outpost/
62
62
 
63
63
  Expected: no matches except this erasure report.
64
64
 
65
- ```
65
+ ```text
66
66
 
67
67
  ```
@@ -18,7 +18,7 @@ limits.
18
18
 
19
19
  Search by skill + availability:
20
20
 
21
- ```
21
+ ```text
22
22
  WebFetch URL: https://api.github.com/search/users?q=%22data+engineering%22+%22open+to+work%22&per_page=30&sort=joined&order=desc
23
23
  WebFetch URL: https://api.github.com/search/users?q=%22full+stack%22+%22available+for+hire%22&per_page=30&sort=joined&order=desc
24
24
  WebFetch URL: https://api.github.com/search/users?q=%22devops%22+%22looking+for%22&per_page=30&sort=joined&order=desc
@@ -26,7 +26,7 @@ WebFetch URL: https://api.github.com/search/users?q=%22devops%22+%22looking+for%
26
26
 
27
27
  Search repos with README signals:
28
28
 
29
- ```
29
+ ```text
30
30
  WebFetch URL: https://api.github.com/search/repositories?q=%22hire+me%22+in:readme&sort=updated&order=desc&per_page=10
31
31
  ```
32
32
 
@@ -38,7 +38,7 @@ Manchester, Edinburgh.
38
38
  Try broader tags: `jobsearch`, `career`, `remotework`, `job`, `hiring`. Or pull
39
39
  from a tag and filter by title/description:
40
40
 
41
- ```
41
+ ```text
42
42
  WebFetch URL: https://dev.to/api/articles?tag=career&per_page=25
43
43
  ```
44
44
 
@@ -6,14 +6,14 @@ Reference for `req-scan` Step 2 (fetch & scan). One source per wake cycle.
6
6
 
7
7
  Monthly thread, posted on the 1st.
8
8
 
9
- ```
9
+ ```text
10
10
  WebFetch URL: https://hn.algolia.com/api/v1/search?query=%22Who+wants+to+be+hired%22&tags=ask_hn&hitsPerPage=5
11
11
  ```
12
12
 
13
13
  The first hit whose title matches "Who wants to be hired?" with `created_at` in
14
14
  the current or previous month is the target thread.
15
15
 
16
- ```
16
+ ```text
17
17
  WebFetch URL: https://hn.algolia.com/api/v1/items/{objectID}
18
18
  ```
19
19
 
@@ -34,7 +34,7 @@ WebFetch URL: https://hn.algolia.com/api/v1/items/{objectID}
34
34
 
35
35
  Search by location (rotate one query per wake):
36
36
 
37
- ```
37
+ ```text
38
38
  WebFetch URL: https://api.github.com/search/users?q=%22open+to+work%22+location:UK&per_page=30&sort=joined&order=desc
39
39
  WebFetch URL: https://api.github.com/search/users?q=%22open+to+work%22+location:Europe&per_page=30&sort=joined&order=desc
40
40
  WebFetch URL: https://api.github.com/search/users?q=%22looking+for+work%22+location:remote&per_page=30&sort=joined&order=desc
@@ -47,7 +47,7 @@ Alternate bio phrases to rotate across wakes: `"available for hire"`,
47
47
 
48
48
  Fetch each promising candidate's full profile:
49
49
 
50
- ```
50
+ ```text
51
51
  WebFetch URL: https://api.github.com/users/{login}
52
52
  ```
53
53
 
@@ -60,7 +60,7 @@ profiles per wake (1 search + 5 profile fetches = 6 requests).
60
60
 
61
61
  ## 3. dev.to
62
62
 
63
- ```
63
+ ```text
64
64
  WebFetch URL: https://dev.to/api/articles?tag=opentowork&per_page=25
65
65
  WebFetch URL: https://dev.to/api/articles?tag=lookingforwork&per_page=25
66
66
  ```
@@ -71,8 +71,8 @@ Process **10 files per run**.
71
71
  ### 1. Load context and pick the batch
72
72
 
73
73
  Read the user's name, email, and domain from
74
- `~/.cache/fit/outpost/state/identity.md` (run the `person-identify` skill first if
75
- it is missing or stale). List new or changed source files:
74
+ `~/.cache/fit/outpost/state/identity.md` (run the `person-identify` skill first
75
+ if it is missing or stale). List new or changed source files:
76
76
 
77
77
  ```bash
78
78
  node .claude/skills/extract-entities/scripts/state.mjs check
@@ -30,7 +30,7 @@ Empty or unrecognized step → default to `new`.
30
30
  The raw `step` value is always preserved in the parser's JSON output and must be
31
31
  stored in the candidate brief's `## Pipeline` section, e.g.
32
32
 
33
- ```
33
+ ```text
34
34
  - **2026-02-10**: Applied via LinkedIn — Step: Manager Request to Move Forward (HS)
35
35
  ```
36
36
 
@@ -41,7 +41,9 @@ Run the sync as a single Node.js script with embedded SQLite. This avoids N+1
41
41
  process invocations (one per event for attendees) and handles all data
42
42
  transformation in one pass:
43
43
 
44
- node scripts/sync.mjs [--days N]
44
+ ```text
45
+ node scripts/sync.mjs [--days N]
46
+ ```
45
47
 
46
48
  - `--days N` — how many days back to sync (default: 30)
47
49
 
@@ -101,7 +103,9 @@ Each `{event_id}.json` file:
101
103
  After syncing, use the query script to filter events by date or time window.
102
104
  **Agents should use this script instead of writing bespoke calendar parsers.**
103
105
 
104
- node scripts/query.mjs [options]
106
+ ```text
107
+ node scripts/query.mjs [options]
108
+ ```
105
109
 
106
110
  ### Time filters (combinable)
107
111
 
@@ -48,7 +48,9 @@ their email.
48
48
  Run the sync as a single Node.js script with embedded SQLite. This avoids N+1
49
49
  process invocations and handles all data transformation in one pass:
50
50
 
51
- node scripts/sync.mjs [--days N]
51
+ ```text
52
+ node scripts/sync.mjs [--days N]
53
+ ```
52
54
 
53
55
  - `--days N` — how many days back to look on first sync (default: 30)
54
56
 
@@ -106,7 +106,7 @@ used by the `recipients` table.
106
106
 
107
107
  Attachment files on disk follow this path structure:
108
108
 
109
- ```
109
+ ```text
110
110
  ~/Library/Mail/V10/.../Attachments/{message_ROWID}/{attachment_id}/{filename}
111
111
  ```
112
112
 
@@ -48,7 +48,9 @@ their Teams chats.
48
48
 
49
49
  Run the sync as a single Node.js script:
50
50
 
51
- node scripts/sync.mjs [--days N]
51
+ ```text
52
+ node scripts/sync.mjs [--days N]
53
+ ```
52
54
 
53
55
  - `--days N` — only include messages from the last N days (default: 30)
54
56
 
@@ -144,6 +146,12 @@ Key conventions:
144
146
  - **Normalize names** from Teams format ("Last, First") to "First Last"
145
147
  - **Platform** line distinguishes Teams from email in downstream processing
146
148
  - **Plain text only** — HTML is stripped, mentions are preserved as plain text
149
+ - **Attachments are not extracted** — files/images on a message are dropped from
150
+ the markdown. They are hosted on SharePoint/OneDrive, not in the local cache.
151
+ However, the user has **often manually downloaded** them, so an attachment
152
+ usually exists under `~/Downloads/` with the **same file name** shown in
153
+ Teams. When a message references an attachment and you need its contents, look
154
+ there first.
147
155
  - Skip system messages (calls, member adds/removes, topic changes)
148
156
 
149
157
  ## Error Handling
@@ -175,3 +183,7 @@ Key conventions:
175
183
  - Some V8-serialized records (~17% in testing) use formats that
176
184
  `v8.deserialize()` cannot decode. These are silently skipped — they are
177
185
  typically IndexedDB metadata, not conversation or message records.
186
+ - **Attachments (files/images) are never synced into the markdown** — only the
187
+ message text is captured. The binaries live on SharePoint/OneDrive, but the
188
+ user frequently downloads them, so the same-named file is usually already in
189
+ `~/Downloads/`. Check there before trying to fetch from SharePoint.
@@ -31,56 +31,89 @@ function readIdbVarint(buf, offset) {
31
31
  return { value: result, bytesRead: pos - offset };
32
32
  }
33
33
 
34
+ // Highest V8 serialization wire-format version Node's bundled v8.deserialize
35
+ // accepts. Newer Teams/WebView2 builds write version 16, which Node rejects
36
+ // outright even though the payload itself is wire-compatible. We patch the
37
+ // version byte down to this value before deserializing. Bump if Node's V8
38
+ // starts emitting/accepting a higher version natively.
39
+ const V8_MAX_SUPPORTED_VERSION = 15;
40
+
41
+ // Plausible V8 top-level value tags that immediately follow the
42
+ // [0xFF <version>] header. Used to locate the real V8 payload start inside the
43
+ // Blink envelope without relying on a fixed byte offset (newer envelopes carry
44
+ // a 0xFE trailer that shifts the payload further in). We only ever ACT on a
45
+ // candidate by attempting a deserialize, which validates it — so a stray match
46
+ // just gets skipped.
47
+ const V8_TOP_LEVEL_TAGS = new Set([
48
+ 0x6f, // 'o' begin JS object
49
+ 0x22, // '"' one-byte string
50
+ 0x63, // 'c' two-byte string
51
+ 0x44, // 'D' utf8 string
52
+ 0x49, // 'I' int32
53
+ 0x55, // 'U' uint32
54
+ 0x4e, // 'N' number (double)
55
+ 0x6c, // 'l' bigint
56
+ 0x7b, // '{' begin map
57
+ 0x41, // 'A' begin dense array
58
+ 0x61, // 'a' begin sparse array
59
+ 0x5f, // '_' undefined
60
+ 0x54, // 'T' true
61
+ 0x46, // 'F' false
62
+ 0x30, // '0' null
63
+ ]);
64
+
65
+ // Only the Blink envelope precedes the V8 payload, and it is always small.
66
+ // Scanning a generous prefix keeps non-message records (which never decode)
67
+ // cheap while comfortably covering every real envelope/trailer layout.
68
+ const V8_START_SCAN_LIMIT = 256;
69
+
34
70
  /**
35
- * Try to deserialize from the second 0xFF marker within the first `limit` bytes.
36
- * The Blink envelope has: [varint wire_size] [0xFF blink_ver] [envelope...] [0xFF v8_ver] [V8 data]
37
- * We want the second 0xFF that starts valid V8 data.
71
+ * Deserialize the V8 payload starting at `off`. Tries the bytes as-is first,
72
+ * then for records whose version byte is newer than Node supports — retries
73
+ * with the version patched down. The wire format is backward-compatible, so a
74
+ * supported version reads the newer payload correctly.
38
75
  */
39
- function deserializeFromSecondMarker(rawValue, limit) {
40
- let ffCount = 0;
41
- for (let i = 0; i < limit; i++) {
42
- if (rawValue[i] !== 0xff) continue;
43
- ffCount++;
44
- if (ffCount >= 2) {
76
+ function deserializeAt(rawValue, off) {
77
+ try {
78
+ return v8.deserialize(rawValue.subarray(off));
79
+ } catch {
80
+ // fall through to version patching
81
+ }
82
+
83
+ const version = rawValue[off + 1];
84
+ if (version > V8_MAX_SUPPORTED_VERSION) {
85
+ const patched = Buffer.from(rawValue.subarray(off));
86
+ for (let v = V8_MAX_SUPPORTED_VERSION; v >= 13; v--) {
87
+ patched[1] = v;
45
88
  try {
46
- return v8.deserialize(rawValue.subarray(i));
89
+ return v8.deserialize(patched);
47
90
  } catch {
48
- // keep scanning
91
+ // try the next-lower version
49
92
  }
50
93
  }
51
94
  }
52
95
  return null;
53
96
  }
54
97
 
55
- /**
56
- * Fallback: try deserializing from every 0xFF position within `limit` bytes.
57
- */
58
- function deserializeFromAnyMarker(rawValue, limit) {
59
- for (let i = 0; i < limit; i++) {
60
- if (rawValue[i] !== 0xff) continue;
61
- try {
62
- return v8.deserialize(rawValue.subarray(i));
63
- } catch {
64
- continue;
65
- }
66
- }
67
- return null;
68
- }
69
-
70
98
  /**
71
99
  * Try to deserialize a Chromium IndexedDB value.
72
- * Values have a Blink envelope before the V8 payload.
73
- * Scans for the V8 version tag (0xFF) and attempts deserialization.
100
+ *
101
+ * Values have a Blink envelope (and, in newer WebView2 builds, a 0xFE trailer)
102
+ * before the V8 payload. Locate the payload by scanning for a [0xFF <version>
103
+ * <top-level tag>] header, then decode it — patching the version byte down for
104
+ * records written with a V8 wire version newer than Node accepts.
74
105
  */
75
106
  function tryDeserialize(rawValue) {
76
107
  if (!rawValue || rawValue.length < 4) return null;
77
108
 
78
- const headerLimit = Math.min(rawValue.length, 60);
79
- const result = deserializeFromSecondMarker(rawValue, headerLimit);
80
- if (result !== null) return result;
81
-
82
- const fallbackLimit = Math.min(rawValue.length, 100);
83
- return deserializeFromAnyMarker(rawValue, fallbackLimit);
109
+ const limit = Math.min(rawValue.length - 2, V8_START_SCAN_LIMIT);
110
+ for (let i = 0; i <= limit; i++) {
111
+ if (rawValue[i] !== 0xff) continue;
112
+ if (!V8_TOP_LEVEL_TAGS.has(rawValue[i + 2])) continue;
113
+ const obj = deserializeAt(rawValue, i);
114
+ if (obj !== null) return obj;
115
+ }
116
+ return null;
84
117
  }
85
118
 
86
119
  /**
@@ -32,7 +32,8 @@ monorepo. "Instructions" means all three surfaces, treated equally:
32
32
  - `.claude/skills/*/SKILL.md` and reference files — skills.
33
33
  - `CHANGELOG.md` (root) — the existing changelog, for what's already recorded.
34
34
  - The changes made in the current working session — the source of truth for what
35
- changed, since the KB lives on a synced filesystem and is not version-controlled.
35
+ changed, since the KB lives on a synced filesystem and is not
36
+ version-controlled.
36
37
 
37
38
  ## Outputs
38
39