@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.
- package/README.md +2 -2
- package/bin/fit-outpost.js +3 -2
- package/package.json +4 -4
- package/src/agent-path.js +10 -6
- package/src/agent-runner.js +28 -24
- package/src/index.js +2 -3
- package/src/kb-manager.js +21 -19
- package/src/outpost.js +37 -33
- package/src/posture.js +3 -3
- package/src/privilege.js +11 -11
- package/src/scheduler.js +14 -12
- package/src/socket-server.js +19 -18
- package/src/spawn-env.js +7 -6
- package/src/state-manager.js +2 -2
- package/templates/.claude/agents/chief-of-staff.md +22 -22
- package/templates/.claude/agents/concierge.md +19 -17
- package/templates/.claude/agents/head-hunter.md +27 -26
- package/templates/.claude/agents/librarian.md +17 -16
- package/templates/.claude/agents/postman.md +19 -18
- package/templates/.claude/agents/recruiter.md +31 -27
- package/templates/.claude/skills/anarlog-follow/SKILL.md +50 -49
- package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
- package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +4 -4
- package/templates/.claude/skills/anarlog-process/SKILL.md +35 -32
- package/templates/.claude/skills/anarlog-process/references/extraction.md +26 -22
- package/templates/.claude/skills/anarlog-process/references/sessions.md +10 -10
- package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +10 -9
- package/templates/.claude/skills/anarlog-trim/SKILL.md +43 -40
- package/templates/.claude/skills/candidate-report/SKILL.md +30 -28
- package/templates/.claude/skills/candidate-report/references/report-template.html +10 -10
- package/templates/.claude/skills/candidate-report/references/report.css +4 -4
- package/templates/.claude/skills/candidate-report/references/rubric.md +15 -14
- package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +2 -2
- package/templates/.claude/skills/changelog/SKILL.md +55 -50
- package/templates/.claude/skills/deck-create/SKILL.md +30 -30
- package/templates/.claude/skills/deck-create/references/slide.css +4 -4
- package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/deck-review/SKILL.md +59 -57
- package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
- package/templates/.claude/skills/deck-summarize/SKILL.md +49 -43
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +4 -3
- package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
- package/templates/.claude/skills/doc-collab/SKILL.md +25 -25
- package/templates/.claude/skills/doc-create/SKILL.md +21 -20
- package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/draft-emails/SKILL.md +37 -33
- package/templates/.claude/skills/draft-emails/references/template.md +2 -2
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +5 -4
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +11 -9
- package/templates/.claude/skills/extract-entities/SKILL.md +65 -65
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +5 -5
- package/templates/.claude/skills/extract-entities/references/conditions.md +14 -13
- package/templates/.claude/skills/extract-entities/references/content.md +10 -9
- package/templates/.claude/skills/extract-entities/references/links.md +5 -5
- package/templates/.claude/skills/extract-entities/references/recruitment.md +19 -18
- package/templates/.claude/skills/extract-entities/references/resolution.md +13 -13
- package/templates/.claude/skills/extract-entities/references/sources.md +8 -8
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +2 -2
- package/templates/.claude/skills/extract-entities/references/templates-priorities.md +2 -2
- package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
- package/templates/.claude/skills/meeting-prep/SKILL.md +25 -24
- package/templates/.claude/skills/organize-files/SKILL.md +24 -22
- package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +5 -4
- package/templates/.claude/skills/organize-files/scripts/summarize.mjs +4 -4
- package/templates/.claude/skills/person-identify/SKILL.md +24 -22
- package/templates/.claude/skills/person-identify/scripts/identify.sh +17 -17
- package/templates/.claude/skills/person-lookup/SKILL.md +28 -29
- package/templates/.claude/skills/person-lookup/scripts/lookup.sh +19 -16
- package/templates/.claude/skills/req-assess/SKILL.md +27 -27
- package/templates/.claude/skills/req-assess/references/interview-template.md +1 -1
- package/templates/.claude/skills/req-assess/references/panel-template.md +3 -2
- package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
- package/templates/.claude/skills/req-decide/SKILL.md +28 -25
- package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
- package/templates/.claude/skills/req-forget/SKILL.md +28 -26
- package/templates/.claude/skills/req-forget/references/classify.md +13 -13
- package/templates/.claude/skills/req-forget/references/locations.md +2 -2
- package/templates/.claude/skills/req-forget/references/report-template.md +1 -1
- package/templates/.claude/skills/req-scan/SKILL.md +21 -22
- package/templates/.claude/skills/req-scan/references/fallbacks.md +9 -9
- package/templates/.claude/skills/req-scan/references/filters.md +13 -13
- package/templates/.claude/skills/req-scan/references/sources.md +7 -7
- package/templates/.claude/skills/req-scan/references/state.md +1 -1
- package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
- package/templates/.claude/skills/req-screen/SKILL.md +31 -29
- package/templates/.claude/skills/req-screen/references/rubric.md +18 -18
- package/templates/.claude/skills/req-screen/references/template.md +4 -4
- package/templates/.claude/skills/req-track/SKILL.md +40 -40
- package/templates/.claude/skills/req-track/references/fields.md +19 -18
- package/templates/.claude/skills/req-track/references/signals.md +12 -12
- package/templates/.claude/skills/req-track/references/statuses.md +3 -2
- package/templates/.claude/skills/req-track/references/templates.md +4 -4
- package/templates/.claude/skills/req-workday/SKILL.md +24 -24
- package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
- package/templates/.claude/skills/req-workday/references/templates.md +13 -13
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +7 -7
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +19 -17
- package/templates/.claude/skills/send-chat/SKILL.md +47 -43
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +20 -19
- package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +7 -7
- package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +6 -6
- package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +9 -8
- package/templates/.claude/skills/sync-apple-mail/SKILL.md +17 -14
- package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +10 -10
- package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +9 -9
- package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +16 -13
- package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +3 -3
- package/templates/.claude/skills/sync-teams/SKILL.md +43 -43
- package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +22 -21
- package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +4 -4
- package/templates/.claude/skills/sync-teams/scripts/sync.mjs +4 -4
- package/templates/.claude/skills/upstream-instructions/SKILL.md +39 -40
- package/templates/.claude/skills/upstream-instructions/references/examples.md +2 -2
- package/templates/CLAUDE.md +40 -37
|
@@ -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
|
|
7
|
-
* text/plain
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
15
|
-
//
|
|
16
|
-
// site is openDb below
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
7
|
-
* sender, recipients, date, body text (parsed from .emlx files), and
|
|
8
|
-
* links.
|
|
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
|
|
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
|
|
11
|
-
|
|
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
|
|
15
|
-
automation, no API tokens, no network access
|
|
16
|
-
|
|
17
|
-
IndexedDB at a known location. This skill parses those files,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
96
|
-
identifiers.
|
|
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) —
|
|
100
|
-
|
|
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
|
|
148
|
-
- **Plain text only** —
|
|
149
|
-
- **
|
|
150
|
-
the markdown.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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,
|
|
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.**
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
- **
|
|
174
|
-
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
-
|
|
182
|
-
|
|
183
|
-
- Some V8-serialized records (~17% in
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
- **
|
|
187
|
-
message text
|
|
188
|
-
user
|
|
189
|
-
`~/Downloads/`. Check there before
|
|
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
|
|
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
|
|
36
|
-
// outright even though the payload itself is wire-compatible. We patch the
|
|
37
|
-
// version byte down to this value before
|
|
38
|
-
//
|
|
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.
|
|
43
|
-
// Blink envelope
|
|
44
|
-
// a 0xFE trailer that shifts the payload further
|
|
45
|
-
//
|
|
46
|
-
//
|
|
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
|
|
66
|
-
//
|
|
67
|
-
// cheap
|
|
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
|
|
72
|
-
*
|
|
73
|
-
* with the version patched down. The wire format is
|
|
74
|
-
* supported version reads the newer payload
|
|
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.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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
|
|
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
|
|
6
|
-
* Handles Snappy-compressed blocks
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
-
|
|
20
|
-
|
|
21
|
-
- The user wants to document what changed locally before
|
|
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
|
|
35
|
-
changed
|
|
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
|
|
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
|
-
- [ ]
|
|
46
|
-
|
|
47
|
-
- [ ]
|
|
48
|
-
- [ ]
|
|
49
|
-
- [ ]
|
|
50
|
-
"updated CLAUDE.md"
|
|
51
|
-
- [ ]
|
|
52
|
-
|
|
53
|
-
- [ ]
|
|
54
|
-
- [ ]
|
|
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
|
|
69
|
-
history to diff. Identify what changed from the work
|
|
70
|
-
|
|
71
|
-
`.claude/agents/`, and `.claude/skills/` during the current
|
|
72
|
-
|
|
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
|
|
75
|
-
existing entry. If something
|
|
76
|
-
or why from the session, flag it
|
|
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
|
|
94
|
-
`CLAUDE.md`, several agents, and several skills
|
|
95
|
-
whose Scope lists every surface touched.
|
|
96
|
-
the
|
|
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
|
|
108
|
-
|
|
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
|
|
143
|
-
- The
|
|
144
|
-
|
|
145
|
-
- When in doubt about whether a change is upstream-worthy, include it
|
|
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
|
|
6
|
-
|
|
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
|
|