@forwardimpact/outpost 3.12.0 → 3.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +2 -2
  2. package/bin/fit-outpost.js +3 -2
  3. package/package.json +4 -4
  4. package/src/agent-path.js +10 -6
  5. package/src/agent-runner.js +28 -24
  6. package/src/index.js +2 -3
  7. package/src/kb-manager.js +21 -19
  8. package/src/outpost.js +37 -33
  9. package/src/posture.js +3 -3
  10. package/src/privilege.js +11 -11
  11. package/src/scheduler.js +14 -12
  12. package/src/socket-server.js +19 -18
  13. package/src/spawn-env.js +7 -6
  14. package/src/state-manager.js +2 -2
  15. package/templates/.claude/agents/chief-of-staff.md +22 -22
  16. package/templates/.claude/agents/concierge.md +19 -17
  17. package/templates/.claude/agents/head-hunter.md +27 -26
  18. package/templates/.claude/agents/librarian.md +17 -16
  19. package/templates/.claude/agents/postman.md +19 -18
  20. package/templates/.claude/agents/recruiter.md +31 -27
  21. package/templates/.claude/skills/anarlog-follow/SKILL.md +50 -49
  22. package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
  23. package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +4 -4
  24. package/templates/.claude/skills/anarlog-process/SKILL.md +35 -32
  25. package/templates/.claude/skills/anarlog-process/references/extraction.md +26 -22
  26. package/templates/.claude/skills/anarlog-process/references/sessions.md +10 -10
  27. package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +10 -9
  28. package/templates/.claude/skills/anarlog-trim/SKILL.md +43 -40
  29. package/templates/.claude/skills/candidate-report/SKILL.md +30 -28
  30. package/templates/.claude/skills/candidate-report/references/report-template.html +10 -10
  31. package/templates/.claude/skills/candidate-report/references/report.css +4 -4
  32. package/templates/.claude/skills/candidate-report/references/rubric.md +15 -14
  33. package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +2 -2
  34. package/templates/.claude/skills/changelog/SKILL.md +55 -50
  35. package/templates/.claude/skills/deck-create/SKILL.md +30 -30
  36. package/templates/.claude/skills/deck-create/references/slide.css +4 -4
  37. package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +6 -6
  38. package/templates/.claude/skills/deck-review/SKILL.md +59 -57
  39. package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
  40. package/templates/.claude/skills/deck-summarize/SKILL.md +49 -43
  41. package/templates/.claude/skills/deck-summarize/references/brief-template.md +4 -3
  42. package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
  43. package/templates/.claude/skills/doc-collab/SKILL.md +25 -25
  44. package/templates/.claude/skills/doc-create/SKILL.md +21 -20
  45. package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
  46. package/templates/.claude/skills/draft-emails/SKILL.md +37 -33
  47. package/templates/.claude/skills/draft-emails/references/template.md +2 -2
  48. package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +5 -4
  49. package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +11 -9
  50. package/templates/.claude/skills/extract-entities/SKILL.md +65 -65
  51. package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +5 -5
  52. package/templates/.claude/skills/extract-entities/references/conditions.md +14 -13
  53. package/templates/.claude/skills/extract-entities/references/content.md +10 -9
  54. package/templates/.claude/skills/extract-entities/references/links.md +5 -5
  55. package/templates/.claude/skills/extract-entities/references/recruitment.md +19 -18
  56. package/templates/.claude/skills/extract-entities/references/resolution.md +13 -13
  57. package/templates/.claude/skills/extract-entities/references/sources.md +8 -8
  58. package/templates/.claude/skills/extract-entities/references/templates-conditions.md +2 -2
  59. package/templates/.claude/skills/extract-entities/references/templates-priorities.md +2 -2
  60. package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
  61. package/templates/.claude/skills/meeting-prep/SKILL.md +25 -24
  62. package/templates/.claude/skills/organize-files/SKILL.md +24 -22
  63. package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +5 -4
  64. package/templates/.claude/skills/organize-files/scripts/summarize.mjs +4 -4
  65. package/templates/.claude/skills/person-identify/SKILL.md +24 -22
  66. package/templates/.claude/skills/person-identify/scripts/identify.sh +17 -17
  67. package/templates/.claude/skills/person-lookup/SKILL.md +28 -29
  68. package/templates/.claude/skills/person-lookup/scripts/lookup.sh +19 -16
  69. package/templates/.claude/skills/req-assess/SKILL.md +27 -27
  70. package/templates/.claude/skills/req-assess/references/interview-template.md +1 -1
  71. package/templates/.claude/skills/req-assess/references/panel-template.md +3 -2
  72. package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
  73. package/templates/.claude/skills/req-decide/SKILL.md +28 -25
  74. package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
  75. package/templates/.claude/skills/req-forget/SKILL.md +28 -26
  76. package/templates/.claude/skills/req-forget/references/classify.md +13 -13
  77. package/templates/.claude/skills/req-forget/references/locations.md +2 -2
  78. package/templates/.claude/skills/req-forget/references/report-template.md +1 -1
  79. package/templates/.claude/skills/req-scan/SKILL.md +21 -22
  80. package/templates/.claude/skills/req-scan/references/fallbacks.md +9 -9
  81. package/templates/.claude/skills/req-scan/references/filters.md +13 -13
  82. package/templates/.claude/skills/req-scan/references/sources.md +7 -7
  83. package/templates/.claude/skills/req-scan/references/state.md +1 -1
  84. package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
  85. package/templates/.claude/skills/req-screen/SKILL.md +31 -29
  86. package/templates/.claude/skills/req-screen/references/rubric.md +18 -18
  87. package/templates/.claude/skills/req-screen/references/template.md +4 -4
  88. package/templates/.claude/skills/req-track/SKILL.md +40 -40
  89. package/templates/.claude/skills/req-track/references/fields.md +19 -18
  90. package/templates/.claude/skills/req-track/references/signals.md +12 -12
  91. package/templates/.claude/skills/req-track/references/statuses.md +3 -2
  92. package/templates/.claude/skills/req-track/references/templates.md +4 -4
  93. package/templates/.claude/skills/req-workday/SKILL.md +24 -24
  94. package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
  95. package/templates/.claude/skills/req-workday/references/templates.md +13 -13
  96. package/templates/.claude/skills/req-workday/references/xlsx-format.md +7 -7
  97. package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +19 -17
  98. package/templates/.claude/skills/send-chat/SKILL.md +47 -43
  99. package/templates/.claude/skills/sync-apple-calendar/SKILL.md +20 -19
  100. package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +7 -7
  101. package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +6 -6
  102. package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +9 -8
  103. package/templates/.claude/skills/sync-apple-mail/SKILL.md +17 -14
  104. package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +10 -10
  105. package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +9 -9
  106. package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +16 -13
  107. package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +3 -3
  108. package/templates/.claude/skills/sync-teams/SKILL.md +43 -43
  109. package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +22 -21
  110. package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +4 -4
  111. package/templates/.claude/skills/sync-teams/scripts/sync.mjs +4 -4
  112. package/templates/.claude/skills/upstream-instructions/SKILL.md +39 -40
  113. package/templates/.claude/skills/upstream-instructions/references/examples.md +2 -2
  114. package/templates/CLAUDE.md +40 -37
@@ -3,11 +3,11 @@
3
3
  * Parse a macOS Mail .emlx or .partial.emlx file and output the plain text body.
4
4
  *
5
5
  * The .emlx format is: first line = byte count, then RFC822 message, then Apple
6
- * plist. This script reads the RFC822 portion, walks MIME parts to find
7
- * text/plain, and prints it to stdout. If the email is HTML-only, falls back to
8
- * stripping tags and decoding entities.
6
+ * plist. This script reads the RFC822 portion. It walks the MIME parts to find
7
+ * text/plain. It prints that part to stdout. If the email is HTML-only, the
8
+ * script strips the tags and decodes the entities instead.
9
9
  *
10
- * Also exports `parseEmlx()` and `extractBody()` for use by sync-apple-mail.
10
+ * It also exports `parseEmlx()` and `extractBody()` for sync-apple-mail.
11
11
  */
12
12
 
13
13
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
@@ -16,7 +16,7 @@ if (process.argv.includes("-h") || process.argv.includes("--help")) {
16
16
  Usage: node scripts/parse-emlx.mjs <path-to-emlx-file> [-h|--help]
17
17
 
18
18
  Parses a macOS Mail .emlx or .partial.emlx file and prints the plain text
19
- body to stdout. Falls back to stripping HTML tags for HTML-only emails.`);
19
+ body to stdout. For an HTML-only email, it strips the HTML tags instead.`);
20
20
  process.exit(0);
21
21
  }
22
22
 
@@ -127,7 +127,7 @@ function parseHeaders(raw) {
127
127
  continue;
128
128
  }
129
129
 
130
- // New header save previous
130
+ // New header. Save the previous one.
131
131
  pushHeader(headers, currentName, currentValue);
132
132
 
133
133
  const colonIdx = line.indexOf(":");
@@ -141,7 +141,7 @@ function parseHeaders(raw) {
141
141
  }
142
142
 
143
143
  /**
144
- * Parse Content-Type header value.
144
+ * Parse a Content-Type header value.
145
145
  * @param {string} value - e.g. 'text/plain; charset="utf-8"; boundary="abc"'
146
146
  * @returns {{ type: string, params: Record<string, string> }}
147
147
  */
@@ -217,7 +217,7 @@ function decodePayload(data, encoding) {
217
217
  }
218
218
 
219
219
  /**
220
- * Decode text from a buffer using the given charset.
220
+ * Decode text from a buffer with the given charset.
221
221
  * @param {Buffer} data
222
222
  * @param {string} [charset]
223
223
  * @returns {string}
@@ -342,7 +342,7 @@ export function extractBody(raw) {
342
342
  */
343
343
  export function parseEmlx(filePath) {
344
344
  const data = readFileSync(filePath);
345
- // First line is the byte count
345
+ // The first line is the byte count
346
346
  const newline = data.indexOf(0x0a);
347
347
  const byteCount = parseInt(data.subarray(0, newline).toString("ascii"), 10);
348
348
  const raw = data.subarray(newline + 1, newline + 1 + byteCount);
@@ -11,9 +11,10 @@ import { basename, join, resolve, sep } from "node:path";
11
11
  import { homedir } from "node:os";
12
12
  import { globSync } from "node:fs";
13
13
 
14
- // node:sqlite is loaded lazily via createRequire so this module can be imported
15
- // in test environments (bun, older node) that lack the built-in. The only call
16
- // site is openDb below; production runs on Node 22+ where node:sqlite resolves.
14
+ // This module loads node:sqlite lazily through createRequire. Test environments
15
+ // (bun, older node) that lack the built-in can then import this module. The
16
+ // only call site is openDb below. Production runs on Node 22+, where
17
+ // node:sqlite resolves.
17
18
  const requireModule = createRequire(import.meta.url);
18
19
 
19
20
  const HOME = homedir();
@@ -31,13 +32,15 @@ export function findDb() {
31
32
  .sort()
32
33
  .reverse();
33
34
  if (paths.length === 0) {
34
- console.error("Error: Apple Mail database not found. Is Mail configured?");
35
+ console.error(
36
+ "Error: Apple Mail database not found. Check that Mail is configured.",
37
+ );
35
38
  process.exit(1);
36
39
  }
37
40
  return paths[0];
38
41
  }
39
42
 
40
- /** Open a read-only SQLite connection, retrying once if the database is locked. */
43
+ /** Open a read-only SQLite connection. Retry once if the database is locked. */
41
44
  export function openDb(dbPath) {
42
45
  const { DatabaseSync } = requireModule("node:sqlite");
43
46
  try {
@@ -51,7 +54,7 @@ export function openDb(dbPath) {
51
54
  }
52
55
  }
53
56
 
54
- /** Execute a SQL query and return all rows, logging errors and returning an empty array on failure. */
57
+ /** Execute a SQL query and return all rows. Log the error and return an empty array on failure. */
55
58
  export function query(db, sql) {
56
59
  try {
57
60
  return db.prepare(sql).all();
@@ -61,7 +64,7 @@ export function query(db, sql) {
61
64
  }
62
65
  }
63
66
 
64
- /** Load the last sync timestamp from disk, falling back to daysBack days ago on first run. */
67
+ /** Load the last sync timestamp from disk. Use daysBack days ago on the first run. */
65
68
  export function loadLastSync(daysBack = 30) {
66
69
  try {
67
70
  const iso = readFileSync(STATE_FILE, "utf-8").trim();
@@ -84,7 +87,7 @@ export function saveSyncState(lastRowid = null) {
84
87
  }
85
88
  }
86
89
 
87
- /** Load the last-seen message ROWID from disk, returning 0 on first run. */
90
+ /** Load the last-seen message ROWID from disk. Return 0 on the first run. */
88
91
  export function loadLastRowid() {
89
92
  try {
90
93
  const val = readFileSync(ROWID_STATE_FILE, "utf-8").trim();
@@ -108,7 +111,7 @@ export function unixToReadable(ts) {
108
111
  }
109
112
  }
110
113
 
111
- /** Detect whether the messages table uses conversation_id or thread_id for threading. */
114
+ /** Detect whether the messages table uses conversation_id or thread_id as the thread column. */
112
115
  export function discoverThreadColumn(db) {
113
116
  const rows = query(db, "PRAGMA table_info(messages);");
114
117
  const columns = new Set(rows.map((r) => r.name));
@@ -137,7 +140,7 @@ export function findChangedThreads(db, threadCol, sinceTs, lastRowid) {
137
140
  );
138
141
  }
139
142
 
140
- /** Fetch all messages in a thread ordered by date, including sender and summary metadata. */
143
+ /** Fetch all messages in a thread ordered by date, with sender and summary metadata. */
141
144
  export function fetchThreadMessages(db, threadCol, tid) {
142
145
  return query(
143
146
  db,
@@ -217,13 +220,13 @@ export function fetchAttachments(db, messageIds) {
217
220
  }
218
221
 
219
222
  const FALLBACK_ATTACHMENT_NAME = "unnamed";
220
- // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional stripping ASCII control bytes from attacker-controlled filenames.
223
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional. It strips ASCII control bytes from attacker-controlled filenames.
221
224
  const CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
222
225
 
223
226
  /**
224
227
  * Coerce an arbitrary `attachments.name` value into a single, non-empty
225
228
  * basename safe to join under a per-thread destDir. Strips path separators
226
- * (POSIX and win32), strips ASCII control bytes, then takes the last
229
+ * (POSIX and win32). Strips ASCII control bytes. Then takes the last
227
230
  * non-empty/non-dot segment. Returns `"unnamed"` for any input that
228
231
  * collapses to empty, `.`, or `..`. Never throws.
229
232
  */
@@ -341,7 +344,7 @@ function copySingleAttachment(
341
344
  }
342
345
  }
343
346
 
344
- /** Copy all attachments for a thread's messages into the cache directory, deduplicating filenames. `attachmentsDir` defaults to the module-level `ATTACHMENTS_DIR`; tests inject a temp directory. */
347
+ /** Copy all attachments for a thread's messages into the cache directory and deduplicate the filenames. `attachmentsDir` defaults to the module-level `ATTACHMENTS_DIR`. Tests inject a temp directory. */
345
348
  export function copyThreadAttachments(
346
349
  threadId,
347
350
  messages,
@@ -3,9 +3,9 @@
3
3
  * Sync Apple Mail threads to ~/.cache/fit/outpost/apple_mail/ as markdown.
4
4
  *
5
5
  * Queries the macOS Mail Envelope Index SQLite database for threads with new
6
- * messages since the last sync. Writes one markdown file per thread containing
7
- * sender, recipients, date, body text (parsed from .emlx files), and attachment
8
- * links. Attachments are copied into a per-thread subdirectory.
6
+ * messages since the last sync. Writes one markdown file per thread. Each file
7
+ * holds the sender, recipients, date, body text (parsed from .emlx files), and
8
+ * attachment links. Copies the attachments into a per-thread subdirectory.
9
9
  *
10
10
  * Requires macOS with Mail app configured and Full Disk Access granted.
11
11
  */
@@ -1,21 +1,21 @@
1
1
  ---
2
2
  name: sync-teams
3
- description: Sync recent Microsoft Teams chat messages into ~/.cache/fit/outpost/teams_chat/ as markdown files by reading the Teams IndexedDB cache from disk. Use on a schedule or when the user asks to sync their Teams chats. Requires macOS with the Teams desktop app installed.
3
+ description: Sync recent Microsoft Teams chat messages into ~/.cache/fit/outpost/teams_chat/ as markdown files. The skill reads the Teams IndexedDB cache from disk. Use on a schedule or when the user asks to sync their Teams chats. Requires macOS with the Teams desktop app installed.
4
4
  compatibility: Requires macOS with Microsoft Teams desktop app (com.microsoft.teams2) installed
5
5
  ---
6
6
 
7
7
  # Sync Teams
8
8
 
9
9
  Sync recent Microsoft Teams chat messages into
10
- `~/.cache/fit/outpost/teams_chat/` as markdown files. This is an automated data
11
- pipeline skill it ingests chat data that other skills (like
10
+ `~/.cache/fit/outpost/teams_chat/` as markdown files. This is an automated skill
11
+ in the data pipeline. It ingests chat data that other skills (like
12
12
  `extract-entities`) consume downstream.
13
13
 
14
- This skill reads the Teams IndexedDB cache directly from disk no browser
15
- automation, no API tokens, no network access needed. The Teams desktop app
16
- (which uses Edge WebView2) stores conversations and messages in a LevelDB-backed
17
- IndexedDB at a known location. This skill parses those files, deserializes the
18
- V8-encoded records, and writes markdown.
14
+ This skill reads the Teams IndexedDB cache directly from disk. It needs no
15
+ browser automation, no API tokens, and no network access. The Teams desktop app
16
+ uses Edge WebView2. The app stores conversations and messages in a
17
+ LevelDB-backed IndexedDB at a known location. This skill parses those files,
18
+ deserializes the V8-encoded records, and writes markdown.
19
19
 
20
20
  ## Trigger
21
21
 
@@ -59,7 +59,7 @@ The script:
59
59
  1. Reads all LevelDB `.ldb` (SSTable) and `.log` (write-ahead log) files from
60
60
  the Teams IndexedDB directory
61
61
  2. Decompresses Snappy-compressed blocks and deserializes V8-encoded values
62
- using Node's built-in `v8.deserialize()`
62
+ with Node's built-in `v8.deserialize()`
63
63
  3. Extracts conversation records (with member lists, topics, chat type) and
64
64
  message records (with sender names, HTML content, timestamps)
65
65
  4. Groups messages by conversation, filters by date window, and converts HTML
@@ -69,7 +69,7 @@ The script:
69
69
 
70
70
  ### Architecture
71
71
 
72
- Three modules, following the same pattern as `sync-apple-mail`:
72
+ Three modules follow the same pattern as `sync-apple-mail`:
73
73
 
74
74
  | Module | Purpose |
75
75
  | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
@@ -87,17 +87,17 @@ data in LevelDB (the same way Chrome does). The key databases are:
87
87
  - **replychain-manager** — stores actual messages: sender display name, HTML
88
88
  content, timestamps, reactions, edit status
89
89
 
90
- Since LevelDB is an append-only format, files can be read while Teams is
91
- running. Newer `.ldb` files supersede older ones for the same records.
90
+ LevelDB is an append-only format. So you can read the files while Teams runs.
91
+ Newer `.ldb` files supersede older ones for the same records.
92
92
 
93
93
  ### Name Resolution
94
94
 
95
- Teams conversation records don't store human-readable member names only orgid
96
- identifiers. Display names are resolved from:
95
+ Teams conversation records do not store human-readable member names. They store
96
+ orgid identifiers only. Resolve display names from:
97
97
 
98
98
  1. **Conversation topic** (for named group chats)
99
- 2. **Message sender names** (`imDisplayName` field) — for 1:1 chats, the chat is
100
- named after the other participant(s)
99
+ 2. **Message sender names** (`imDisplayName` field) — a 1:1 chat takes the name
100
+ of the other participant(s)
101
101
 
102
102
  ## Output Format
103
103
 
@@ -144,14 +144,13 @@ Key conventions:
144
144
 
145
145
  - Messages in **chronological order** (oldest first)
146
146
  - **Normalize names** from Teams format ("Last, First") to "First Last"
147
- - **Platform** line distinguishes Teams from email in downstream processing
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
+ - The **Platform** line distinguishes Teams from email downstream
148
+ - **Plain text only** — the script strips HTML. Mentions stay as plain text
149
+ - **The script does not extract attachments** — it drops files and images from
150
+ the markdown. SharePoint or OneDrive hosts them. The local cache does not. But
151
+ the user **often downloads them manually**, so an attachment usually exists
152
+ under `~/Downloads/` with the **same file name** Teams shows. When a message
153
+ references an attachment and you need its contents, look there first.
155
154
  - Skip system messages (calls, member adds/removes, topic changes)
156
155
 
157
156
  ## Error Handling
@@ -161,29 +160,30 @@ Key conventions:
161
160
  - LevelDB file parse error → skip that file, continue with others
162
161
  - V8 deserialization failure → skip that record, continue
163
162
  - Snappy decompression failure → skip that block, continue
164
- - Empty chat (no messages in window) → skip, don't write a file
163
+ - Empty chat (no messages in window) → skip, do not write a file
165
164
  - Always update sync state, even on partial success
166
165
 
167
166
  ## Constraints
168
167
 
169
- - **Read-only.** Never writes to the Teams IndexedDB or sends messages.
170
- - **Cache-dependent.** Only conversations cached locally by Teams are available.
171
- This covers recently viewed chats, not full history.
172
- - **Both 1:1 and group chats** are synced (channels are excluded).
173
- - **No message limit per chat** all cached messages within the `--days` window
174
- are included.
168
+ - **Read-only.** The script never writes to the Teams IndexedDB or sends
169
+ messages.
170
+ - **Cache-dependent.** Only the conversations Teams caches locally are
171
+ available. This covers recently viewed chats. It does not cover full history.
172
+ - **Both 1:1 and group chats.** The script syncs both and excludes channels.
173
+ - **No message limit per chat** — the output holds every cached message within
174
+ the `--days` window.
175
175
 
176
176
  ## Limitations
177
177
 
178
- - The IndexedDB is a **cache, not an archive**. Only conversations the user has
179
- recently opened in Teams will have cached message data. Older conversations
180
- that haven't been opened may have conversation metadata but no messages.
181
- - Clearing the Teams cache (a common troubleshooting step) will temporarily
182
- remove all local data until Teams rebuilds it from the server.
183
- - Some V8-serialized records (~17% in testing) use formats that
184
- `v8.deserialize()` cannot decode. These are silently skipped they are
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.
178
+ - The IndexedDB is a **cache**. It is not an archive. Only the conversations the
179
+ user opened recently in Teams have cached message data. An older conversation
180
+ the user never opened may have conversation metadata but no messages.
181
+ - If you clear the Teams cache (a common troubleshooting step), all local data
182
+ goes away until Teams rebuilds it from the server.
183
+ - Some V8-serialized records (~17% in tests) use formats that `v8.deserialize()`
184
+ cannot decode. The script skips them silently. They are typically IndexedDB
185
+ metadata. They are not conversation or message records.
186
+ - **The script never syncs attachments (files/images) into the markdown** — it
187
+ captures only the message text. The binaries live on SharePoint/OneDrive. But
188
+ the user downloads them often, so the same-named file is usually already in
189
+ `~/Downloads/`. Check there before you fetch from SharePoint.
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Chromium stores IndexedDB data in LevelDB with a specific key encoding
6
6
  * (database ID, object store ID) and V8-serialized values wrapped in a Blink
7
- * envelope. This module handles the key parsing and value deserialization.
7
+ * envelope. This module parses the keys and deserializes the values.
8
8
  *
9
9
  * Exports: readIndexedDb(dir) → { conversations: Map, messages: Map }
10
10
  */
@@ -32,18 +32,18 @@ function readIdbVarint(buf, offset) {
32
32
  }
33
33
 
34
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.
35
+ // accepts. Newer Teams/WebView2 builds write version 16. Node rejects it
36
+ // outright, even though the payload itself is wire-compatible. We patch the
37
+ // version byte down to this value before we deserialize. Bump this value when
38
+ // Node's V8 emits or accepts a higher version natively.
39
39
  const V8_MAX_SUPPORTED_VERSION = 15;
40
40
 
41
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.
42
+ // [0xFF <version>] header. The reader uses them to locate where the real V8
43
+ // payload starts inside the Blink envelope. It does not depend on a fixed byte
44
+ // offset (newer envelopes carry a 0xFE trailer that shifts the payload further
45
+ // in). We only ever ACT on a candidate when we try to deserialize it. That
46
+ // call validates the candidate, so the code skips a stray match.
47
47
  const V8_TOP_LEVEL_TAGS = new Set([
48
48
  0x6f, // 'o' begin JS object
49
49
  0x22, // '"' one-byte string
@@ -62,16 +62,17 @@ const V8_TOP_LEVEL_TAGS = new Set([
62
62
  0x30, // '0' null
63
63
  ]);
64
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.
65
+ // Only the Blink envelope precedes the V8 payload. The envelope is always
66
+ // small. A scan over a generous prefix keeps non-message records (which never
67
+ // decode) cheap. It also covers every real envelope/trailer layout comfortably.
68
68
  const V8_START_SCAN_LIMIT = 256;
69
69
 
70
70
  /**
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.
71
+ * Deserialize the V8 payload that starts at `off`. Tries the bytes as-is
72
+ * first. For a record whose version byte is newer than Node supports, it then
73
+ * retries with the version patched down. The wire format is
74
+ * backward-compatible, so a supported version reads the newer payload
75
+ * correctly.
75
76
  */
76
77
  function deserializeAt(rawValue, off) {
77
78
  try {
@@ -99,9 +100,9 @@ function deserializeAt(rawValue, off) {
99
100
  * Try to deserialize a Chromium IndexedDB value.
100
101
  *
101
102
  * 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.
103
+ * before the V8 payload. Scan for a [0xFF <version> <top-level tag>] header to
104
+ * locate the payload. Then decode it. For a record written with a V8 wire
105
+ * version newer than Node accepts, patch the version byte down first.
105
106
  */
106
107
  function tryDeserialize(rawValue) {
107
108
  if (!rawValue || rawValue.length < 4) return null;
@@ -141,7 +142,7 @@ function _parseKeyPrefix(key) {
141
142
  */
142
143
  export function readIndexedDb(dir) {
143
144
  // Use Maps so later entries (from newer .ldb files) overwrite older ones.
144
- // LevelDB reads files in ascending order newer compactions have higher
145
+ // LevelDB reads files in ascending order. Newer compactions have higher
145
146
  // numbers, so the last write for a given key is the most current.
146
147
  const convMap = new Map();
147
148
  const msgMap = new Map();
@@ -2,8 +2,8 @@
2
2
  /**
3
3
  * Read LevelDB SSTable (.ldb) and write-ahead log (.log) files.
4
4
  *
5
- * Parses the binary SSTable format directly no LevelDB library needed.
6
- * Handles Snappy-compressed blocks via snappyjs (pure JS, zero native deps).
5
+ * Parses the binary SSTable format directly. It needs no LevelDB library.
6
+ * Handles Snappy-compressed blocks with snappyjs (pure JS, zero native deps).
7
7
  *
8
8
  * Exports a single generator function: readAllEntries(directory)
9
9
  */
@@ -146,7 +146,7 @@ const LOG_BLOCK_SIZE = 32768;
146
146
  const LOG_HEADER_SIZE = 7; // checksum(4) + length(2) + type(1)
147
147
 
148
148
  /**
149
- * Parse records from a single WAL block, yielding { payload, type } for each.
149
+ * Parse records from a single WAL block. Yield { payload, type } for each one.
150
150
  */
151
151
  function* parseLogBlock(fileData, blockStart, blockEnd) {
152
152
  let offset = blockStart;
@@ -244,7 +244,7 @@ function* parseWriteBatchEntries(batchData) {
244
244
 
245
245
  yield { key, value };
246
246
  } else if (tag === 0) {
247
- // Delete skip the key
247
+ // Delete. Skip the key.
248
248
  const keyLen = readVarint(batchData, pos);
249
249
  pos += keyLen.bytesRead;
250
250
  pos += keyLen.value;
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Sync Microsoft Teams chat messages to ~/.cache/fit/outpost/teams_chat/ as
4
- * markdown files. Reads the Teams IndexedDB cache (LevelDB on disk) directly
5
- * no browser automation, no API tokens, no network access needed.
4
+ * markdown files. Reads the Teams IndexedDB cache (LevelDB on disk) directly.
5
+ * It needs no browser automation, no API tokens, and no network access.
6
6
  *
7
7
  * Requires macOS with the Microsoft Teams desktop app installed.
8
8
  * Requires snappyjs: npm install snappyjs
@@ -272,7 +272,7 @@ function loadUserIdentity() {
272
272
  const nameMatch = identityMd.match(/\*\*Name:\*\*\s*(.+)/);
273
273
  if (nameMatch) return nameMatch[1].trim();
274
274
  } catch {
275
- // identity cache not found or not readable run the identify-user skill
275
+ // identity cache not found or not readable. Run the identify-user skill
276
276
  }
277
277
  return "";
278
278
  }
@@ -326,7 +326,7 @@ function main() {
326
326
 
327
327
  if (!existsSync(TEAMS_IDB_DIR)) {
328
328
  console.error(
329
- "Error: Teams IndexedDB not found. Is the Teams desktop app installed?",
329
+ "Error: Teams IndexedDB not found. Check that the Teams desktop app is installed.",
330
330
  );
331
331
  console.error(`Expected: ${TEAMS_IDB_DIR}`);
332
332
  process.exit(1);
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: upstream-instructions
3
- description: Track changes made to this installation's instruction files the root CLAUDE.md, agent profiles, and skills and record them in a single root CHANGELOG.md so improvements can be contributed back to the upstream monorepo. Use when CLAUDE.md, agents, or skills have been modified, added, or removed locally and those changes should be documented for upstream.
3
+ description: Track changes to this installation's instruction files: the root CLAUDE.md, agent profiles, and skills. Record them in a single root CHANGELOG.md so you can contribute improvements back to the upstream monorepo. Use when someone modified, added, or removed CLAUDE.md, agents, or skills locally and you should document those changes for upstream.
4
4
  ---
5
5
 
6
6
  # Upstream Instructions
7
7
 
8
- Track changes to this installation's **instructions** and record them in one
9
- root `CHANGELOG.md` so improvements can be contributed back to the upstream
10
- monorepo. "Instructions" means all three surfaces, treated equally:
8
+ Track changes to this installation's **instructions**. Record them in one root
9
+ `CHANGELOG.md` so you can contribute improvements back to the upstream monorepo.
10
+ "Instructions" means all three surfaces, and they carry equal weight:
11
11
 
12
12
  - **`CLAUDE.md`** (root) — installation-wide instructions.
13
13
  - **`.claude/agents/*.md`** — agent profiles.
@@ -16,9 +16,9 @@ monorepo. "Instructions" means all three surfaces, treated equally:
16
16
  ## Trigger
17
17
 
18
18
  - The user asks to prepare local instruction changes for upstream contribution.
19
- - The root `CLAUDE.md`, an agent profile, or a skill has been modified, added,
20
- or removed.
21
- - The user wants to document what changed locally before syncing upstream.
19
+ - Someone modified, added, or removed the root `CLAUDE.md`, an agent profile, or
20
+ a skill.
21
+ - The user wants to document what changed locally before the sync upstream.
22
22
 
23
23
  ## Prerequisites
24
24
 
@@ -31,27 +31,26 @@ monorepo. "Instructions" means all three surfaces, treated equally:
31
31
  - `.claude/agents/*.md` — agent profiles.
32
32
  - `.claude/skills/*/SKILL.md` and reference files — skills.
33
33
  - `CHANGELOG.md` (root) — the existing changelog, for what's already recorded.
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
36
- version-controlled.
34
+ - The changes you made in the current working session — the source of truth for
35
+ what changed. The KB lives on a synced filesystem. It has no version control.
37
36
 
38
37
  ## Outputs
39
38
 
40
- - `CHANGELOG.md` (root) — a **single** reverse-chronological changelog covering
41
- all three surfaces. There are no per-skill or per-agent changelogs.
39
+ - `CHANGELOG.md` (root) — a **single** reverse-chronological changelog that
40
+ covers all three surfaces. There are no per-skill or per-agent changelogs.
42
41
 
43
42
  <do_confirm_checklist goal="Verify the changelog is upstream-ready">
44
43
 
45
- - [ ] Exactly one root `CHANGELOG.md`; no stray per-skill / per-agent changelogs
46
- left behind.
47
- - [ ] Every entry names its **Scope** (which surface(s) and file(s) it touched).
48
- - [ ] Each entry has **What**, **Why**, and **Details**.
49
- - [ ] Descriptions are specific enough for an upstream maintainer to act on (not
50
- "updated CLAUDE.md" / "fixed stuff").
51
- - [ ] New skills/agents include a brief description of their purpose; removed
52
- ones explain why.
53
- - [ ] Dates are the date the change was made, not guessed.
54
- - [ ] No duplicate entries for the same change.
44
+ - [ ] Keep exactly one root `CHANGELOG.md`. Leave no stray per-skill or
45
+ per-agent changelog behind.
46
+ - [ ] Name the **Scope** in every entry (the surfaces and files it touched).
47
+ - [ ] Give each entry **What**, **Why**, and **Details**.
48
+ - [ ] Make each description specific enough for an upstream maintainer to act
49
+ on. Never write "updated CLAUDE.md" or "fixed stuff".
50
+ - [ ] Briefly describe the purpose of each new skill or agent. Explain why you
51
+ removed each deleted one.
52
+ - [ ] Use the real date of the change. Never guess a date.
53
+ - [ ] Write no duplicate entry for the same change.
55
54
 
56
55
  </do_confirm_checklist>
57
56
 
@@ -65,15 +64,15 @@ head -20 CHANGELOG.md 2>/dev/null # newest date already recorded, if any
65
64
 
66
65
  ### 2. Identify changed instructions
67
66
 
68
- Knowledge bases live on a synced filesystem, not in Git, so there is no commit
69
- history to diff. Identify what changed from the work just done **this session**:
70
- recall every edit, addition, removal, and rename made to `CLAUDE.md`,
71
- `.claude/agents/`, and `.claude/skills/` during the current conversation, and
72
- list them per surface.
67
+ Knowledge bases live on a synced filesystem. They do not live in Git, so there
68
+ is no commit history to diff. Identify what changed from the work you did **this
69
+ session**. Recall every edit, addition, removal, and rename you made to
70
+ `CLAUDE.md`, `.claude/agents/`, and `.claude/skills/` during the current
71
+ conversation. List them per surface.
73
72
 
74
- Use `CHANGELOG.md` only to see what's already recorded so you don't duplicate an
75
- existing entry. If something was clearly changed but you can't reconstruct what
76
- or why from the session, flag it as needing review rather than guessing.
73
+ Use `CHANGELOG.md` only to see what it already records. Do not duplicate an
74
+ existing entry. If something clearly changed but you cannot
75
+ reconstruct what or why from the session, flag it for review. Do not guess.
77
76
 
78
77
  ### 3. Classify each change
79
78
 
@@ -90,10 +89,10 @@ or why from the session, flag it as needing review rather than guessing.
90
89
  | `removed` | Instruction file or directory deleted |
91
90
  | `renamed` | File or directory renamed |
92
91
 
93
- A single change often spans surfaces (e.g. a KB-structure change touching
94
- `CLAUDE.md`, several agents, and several skills) record it as **one entry**
95
- whose Scope lists every surface touched. Re-read the affected files to confirm
96
- the change landed as intended before describing it.
92
+ A single change often spans surfaces. For example, a KB-structure change touches
93
+ `CLAUDE.md`, several agents, and several skills. Record it as **one entry**
94
+ whose Scope lists every surface it touched. Before you describe the change,
95
+ re-read the affected files and confirm it landed as intended.
97
96
 
98
97
  ### 4. Describe each change
99
98
 
@@ -104,8 +103,8 @@ Every entry must answer:
104
103
  3. **Details** — a summary of the actual change (not a full diff).
105
104
 
106
105
  Good: "Agents now read `Knowledge/Priorities/` at the start of every wake and
107
- flag anything that threatens a priority drafts and triage were ignoring
108
- strategic context."
106
+ flag anything that threatens a priority. Drafts and triage ignored strategic
107
+ context."
109
108
 
110
109
  Bad: "Updated the agents" / "Fixed stuff" / "Changed line 42".
111
110
 
@@ -139,8 +138,8 @@ Worked examples in [references/examples.md](references/examples.md).
139
138
 
140
139
  ## Notes
141
140
 
142
- - This skill **documents only** it does not push or merge anything.
143
- - The single root `CHANGELOG.md` is consumed by the **downstream-instructions**
144
- skill in the upstream monorepo.
145
- - When in doubt about whether a change is upstream-worthy, include it; the
141
+ - This skill **documents only**. It does not push or merge anything.
142
+ - The **downstream-instructions** skill in the upstream monorepo consumes the
143
+ single root `CHANGELOG.md`.
144
+ - When in doubt about whether a change is upstream-worthy, include it. The
146
145
  upstream maintainer decides what to incorporate.
@@ -2,8 +2,8 @@
2
2
 
3
3
  Reference output for `upstream-instructions` Step 5. There is **one** root
4
4
  `CHANGELOG.md` with reverse-chronological entries. Each entry's **Scope** names
5
- the surface(s) it touched `CLAUDE.md`, `agent:<name>`, `skill:<name>` and a
6
- single entry may span several.
5
+ the surfaces it touched: `CLAUDE.md`, `agent:<name>`, `skill:<name>`. A single
6
+ entry may span several surfaces.
7
7
 
8
8
  ## Cross-surface change (one change, many files)
9
9