@forwardimpact/outpost 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +15 -0
- package/bin/fit-outpost.js +3 -0
- package/config/scheduler.json +34 -0
- package/package.json +63 -0
- package/src/agent-runner.js +189 -0
- package/src/index.js +5 -0
- package/src/kb-manager.js +228 -0
- package/src/outpost.js +417 -0
- package/src/scheduler.js +195 -0
- package/src/socket-server.js +336 -0
- package/src/state-manager.js +126 -0
- package/templates/.claude/agents/chief-of-staff.md +58 -0
- package/templates/.claude/agents/concierge.md +46 -0
- package/templates/.claude/agents/head-hunter.md +57 -0
- package/templates/.claude/agents/librarian.md +60 -0
- package/templates/.claude/agents/postman.md +47 -0
- package/templates/.claude/agents/recruiter.md +63 -0
- package/templates/.claude/settings.json +88 -0
- package/templates/.claude/skills/candidate-report/SKILL.md +139 -0
- package/templates/.claude/skills/candidate-report/references/report-template.html +203 -0
- package/templates/.claude/skills/candidate-report/references/report.css +438 -0
- package/templates/.claude/skills/candidate-report/references/rubric.md +76 -0
- package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +48 -0
- package/templates/.claude/skills/deck-create/SKILL.md +75 -0
- package/templates/.claude/skills/deck-create/references/slide.css +35 -0
- package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +47 -0
- package/templates/.claude/skills/deck-summarize/SKILL.md +139 -0
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +85 -0
- package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +210 -0
- package/templates/.claude/skills/doc-collab/SKILL.md +112 -0
- package/templates/.claude/skills/doc-create/SKILL.md +105 -0
- package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +50 -0
- package/templates/.claude/skills/draft-emails/SKILL.md +141 -0
- package/templates/.claude/skills/draft-emails/references/template.md +26 -0
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +66 -0
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +139 -0
- package/templates/.claude/skills/extract-entities/SKILL.md +187 -0
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +14 -0
- package/templates/.claude/skills/extract-entities/references/conditions.md +55 -0
- package/templates/.claude/skills/extract-entities/references/content.md +104 -0
- package/templates/.claude/skills/extract-entities/references/links.md +48 -0
- package/templates/.claude/skills/extract-entities/references/recruitment.md +57 -0
- package/templates/.claude/skills/extract-entities/references/resolution.md +71 -0
- package/templates/.claude/skills/extract-entities/references/sources.md +70 -0
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +36 -0
- package/templates/.claude/skills/extract-entities/references/templates-goals-priorities.md +63 -0
- package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +67 -0
- package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +68 -0
- package/templates/.claude/skills/extract-entities/scripts/state.mjs +131 -0
- package/templates/.claude/skills/hyprnote-follow/SKILL.md +174 -0
- package/templates/.claude/skills/hyprnote-follow/references/coaching.md +118 -0
- package/templates/.claude/skills/hyprnote-follow/references/debrief.md +59 -0
- package/templates/.claude/skills/hyprnote-follow/references/meeting-types.md +14 -0
- package/templates/.claude/skills/hyprnote-follow/scripts/follow.mjs +243 -0
- package/templates/.claude/skills/hyprnote-process/SKILL.md +147 -0
- package/templates/.claude/skills/hyprnote-process/references/extraction.md +84 -0
- package/templates/.claude/skills/hyprnote-process/references/sessions.md +81 -0
- package/templates/.claude/skills/hyprnote-process/scripts/scan.mjs +246 -0
- package/templates/.claude/skills/hyprnote-trim/SKILL.md +177 -0
- package/templates/.claude/skills/manage-tasks/SKILL.md +152 -0
- package/templates/.claude/skills/manage-tasks/references/format.md +73 -0
- package/templates/.claude/skills/meeting-prep/SKILL.md +180 -0
- package/templates/.claude/skills/organize-files/SKILL.md +146 -0
- package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +105 -0
- package/templates/.claude/skills/organize-files/scripts/summarize.mjs +91 -0
- package/templates/.claude/skills/req-assess/SKILL.md +137 -0
- package/templates/.claude/skills/req-assess/references/interview-template.md +62 -0
- package/templates/.claude/skills/req-assess/references/panel-template.md +117 -0
- package/templates/.claude/skills/req-assess/references/rubric.md +65 -0
- package/templates/.claude/skills/req-decide/SKILL.md +163 -0
- package/templates/.claude/skills/req-decide/references/rubric.md +63 -0
- package/templates/.claude/skills/req-decide/references/template.md +118 -0
- package/templates/.claude/skills/req-forget/SKILL.md +154 -0
- package/templates/.claude/skills/req-forget/references/classify.md +23 -0
- package/templates/.claude/skills/req-forget/references/locations.md +59 -0
- package/templates/.claude/skills/req-forget/references/report-template.md +67 -0
- package/templates/.claude/skills/req-scan/SKILL.md +145 -0
- package/templates/.claude/skills/req-scan/references/fallbacks.md +76 -0
- package/templates/.claude/skills/req-scan/references/filters.md +83 -0
- package/templates/.claude/skills/req-scan/references/sources.md +76 -0
- package/templates/.claude/skills/req-scan/references/state.md +54 -0
- package/templates/.claude/skills/req-scan/references/template.md +36 -0
- package/templates/.claude/skills/req-scan/scripts/state.mjs +396 -0
- package/templates/.claude/skills/req-screen/SKILL.md +151 -0
- package/templates/.claude/skills/req-screen/references/rubric.md +94 -0
- package/templates/.claude/skills/req-screen/references/template.md +91 -0
- package/templates/.claude/skills/req-track/SKILL.md +189 -0
- package/templates/.claude/skills/req-track/references/fields.md +114 -0
- package/templates/.claude/skills/req-track/references/signals.md +42 -0
- package/templates/.claude/skills/req-track/references/statuses.md +32 -0
- package/templates/.claude/skills/req-track/references/templates.md +127 -0
- package/templates/.claude/skills/req-workday/SKILL.md +162 -0
- package/templates/.claude/skills/req-workday/references/field-mapping.md +40 -0
- package/templates/.claude/skills/req-workday/references/status-mapping.md +38 -0
- package/templates/.claude/skills/req-workday/references/templates.md +126 -0
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +56 -0
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +313 -0
- package/templates/.claude/skills/send-chat/SKILL.md +170 -0
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +145 -0
- package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +80 -0
- package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +307 -0
- package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +332 -0
- package/templates/.claude/skills/sync-apple-mail/SKILL.md +151 -0
- package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +115 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +389 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +372 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +239 -0
- package/templates/.claude/skills/sync-teams/SKILL.md +177 -0
- package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +147 -0
- package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +288 -0
- package/templates/.claude/skills/sync-teams/scripts/sync.mjs +381 -0
- package/templates/.claude/skills/upstream-skill/SKILL.md +130 -0
- package/templates/.claude/skills/upstream-skill/references/examples.md +80 -0
- package/templates/.claude/skills/weekly-update/SKILL.md +141 -0
- package/templates/.claude/skills/weekly-update/references/template.md +55 -0
- package/templates/CLAUDE.md +134 -0
- package/templates/USER.md +3 -0
- package/templates/apm.yml +5 -0
- package/templates/knowledge/Briefings/.gitkeep +0 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
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.
|
|
6
|
+
*
|
|
7
|
+
* Requires macOS with the Microsoft Teams desktop app installed.
|
|
8
|
+
* Requires snappyjs: npm install snappyjs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
12
|
+
console.log(`sync-teams — sync Teams chat messages to markdown
|
|
13
|
+
|
|
14
|
+
Usage: node scripts/sync.mjs [--days N] [-h|--help]
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--days N Only include messages from the last N days (default: 30)
|
|
18
|
+
-h, --help Show this help message and exit
|
|
19
|
+
|
|
20
|
+
Reads the Teams IndexedDB cache from disk. Requires macOS with Teams installed.`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { homedir } from "node:os";
|
|
27
|
+
import { readIndexedDb } from "./idb-reader.mjs";
|
|
28
|
+
|
|
29
|
+
const HOME = homedir();
|
|
30
|
+
const OUTDIR = join(HOME, ".cache/fit/outpost/teams_chat");
|
|
31
|
+
const STATE_DIR = join(HOME, ".cache/fit/outpost/state");
|
|
32
|
+
const SYNC_STATE_FILE = join(STATE_DIR, "teams_last_sync");
|
|
33
|
+
const INDEX_FILE = join(STATE_DIR, "teams_chat_index.tsv");
|
|
34
|
+
|
|
35
|
+
const TEAMS_IDB_DIR = join(
|
|
36
|
+
HOME,
|
|
37
|
+
"Library/Containers/com.microsoft.teams2/Data/Library/Application Support",
|
|
38
|
+
"Microsoft/MSTeams/EBWebView/WV2Profile_tfw/IndexedDB",
|
|
39
|
+
"https_teams.microsoft.com_0.indexeddb.leveldb",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
function normalizeName(name) {
|
|
43
|
+
if (!name) return "";
|
|
44
|
+
const trimmed = name.trim();
|
|
45
|
+
if (trimmed.includes(",")) {
|
|
46
|
+
const [last, ...first] = trimmed.split(",").map((s) => s.trim());
|
|
47
|
+
const firstPart = first.join(" ").trim();
|
|
48
|
+
if (firstPart && last) return `${firstPart} ${last}`;
|
|
49
|
+
return last || firstPart;
|
|
50
|
+
}
|
|
51
|
+
return trimmed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toSlug(name) {
|
|
55
|
+
const slug = name
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
58
|
+
.replace(/^-|-$/g, "");
|
|
59
|
+
return slug.slice(0, 80);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function htmlToText(html) {
|
|
63
|
+
if (!html) return "";
|
|
64
|
+
let text = html;
|
|
65
|
+
text = text.replace(/<(style|script)[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
66
|
+
text = text.replace(/<br\s*\/?>/gi, "\n");
|
|
67
|
+
text = text.replace(/<\/p>/gi, "\n");
|
|
68
|
+
text = text.replace(/<\/div>/gi, "\n");
|
|
69
|
+
text = text.replace(/<\/li>/gi, "\n");
|
|
70
|
+
text = text.replace(
|
|
71
|
+
/<span[^>]*itemtype="http:\/\/schema\.skype\.com\/Mention"[^>]*>([^<]*)<\/span>/gi,
|
|
72
|
+
"$1",
|
|
73
|
+
);
|
|
74
|
+
text = text.replace(/<[^>]+>/g, "");
|
|
75
|
+
text = text.replace(/&/g, "&");
|
|
76
|
+
text = text.replace(/</g, "<");
|
|
77
|
+
text = text.replace(/>/g, ">");
|
|
78
|
+
text = text.replace(/"/g, '"');
|
|
79
|
+
text = text.replace(/ /g, " ");
|
|
80
|
+
text = text.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
|
|
81
|
+
text = text.replace(/[ \t]+/g, " ");
|
|
82
|
+
text = text.replace(/\n{3,}/g, "\n\n");
|
|
83
|
+
return text.trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatTimestamp(ts) {
|
|
87
|
+
if (!ts) return "Unknown";
|
|
88
|
+
const ms = typeof ts === "string" ? parseInt(ts, 10) : ts;
|
|
89
|
+
if (isNaN(ms)) return "Unknown";
|
|
90
|
+
const dt = new Date(ms);
|
|
91
|
+
if (isNaN(dt.getTime())) return "Unknown";
|
|
92
|
+
return dt
|
|
93
|
+
.toISOString()
|
|
94
|
+
.replace("T", " ")
|
|
95
|
+
.replace(/\.\d+Z/, "");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _formatDateOnly(ts) {
|
|
99
|
+
if (!ts) return "Unknown";
|
|
100
|
+
const ms = typeof ts === "string" ? parseInt(ts, 10) : ts;
|
|
101
|
+
const dt = new Date(ms);
|
|
102
|
+
if (isNaN(dt.getTime())) return "Unknown";
|
|
103
|
+
return dt.toISOString().slice(0, 10);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isSystemMessage(msg) {
|
|
107
|
+
const type = msg.messageType ?? msg.messagetype ?? "";
|
|
108
|
+
if (type === "Event/Call" || type === "ThreadActivity/AddMember") return true;
|
|
109
|
+
if (type === "ThreadActivity/DeleteMember") return true;
|
|
110
|
+
if (type === "ThreadActivity/TopicUpdate") return true;
|
|
111
|
+
|
|
112
|
+
const content = msg.content ?? "";
|
|
113
|
+
if (content.includes("has made the chat a meeting")) return true;
|
|
114
|
+
if (content.includes("added") && content.includes("to the chat")) return true;
|
|
115
|
+
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function is1to1Chat(conv) {
|
|
120
|
+
const id = conv.id ?? "";
|
|
121
|
+
if (id.startsWith("48:")) return false;
|
|
122
|
+
if (
|
|
123
|
+
conv.threadProperties?.isGroup === "True" ||
|
|
124
|
+
conv.threadProperties?.isGroup === "true"
|
|
125
|
+
)
|
|
126
|
+
return false;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isGroupChat(conv) {
|
|
131
|
+
if (is1to1Chat(conv)) return false;
|
|
132
|
+
const id = conv.id ?? "";
|
|
133
|
+
if (id.includes("@thread.tacv2")) return true;
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function looksLikeOrgId(name) {
|
|
138
|
+
return /^8:orgid:[0-9a-f-]+$/i.test(name.trim());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getChatDisplayName(conv) {
|
|
142
|
+
const topic = conv.threadProperties?.topic ?? conv.properties?.topic ?? null;
|
|
143
|
+
if (topic) return topic;
|
|
144
|
+
|
|
145
|
+
const members = conv.members ?? [];
|
|
146
|
+
const otherMembers = members.filter((m) => {
|
|
147
|
+
const isSelf = m.isSelf || m.isSelf === "true" || m.isSelf === true;
|
|
148
|
+
return !isSelf;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (otherMembers.length > 0) {
|
|
152
|
+
const names = otherMembers
|
|
153
|
+
.map((m) => {
|
|
154
|
+
const raw = m.friendlyName ?? m.displayName ?? "";
|
|
155
|
+
if (!raw || looksLikeOrgId(raw)) return null;
|
|
156
|
+
return normalizeName(raw);
|
|
157
|
+
})
|
|
158
|
+
.filter(Boolean);
|
|
159
|
+
if (names.length > 0) return names.join(", ");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function extractParticipantsFromMessages(chatMessages) {
|
|
166
|
+
const senders = new Set();
|
|
167
|
+
for (const msg of chatMessages) {
|
|
168
|
+
if (msg.sender) senders.add(msg.sender);
|
|
169
|
+
}
|
|
170
|
+
return [...senders];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getChatDisplayNameFromMessages(chatMessages, userIdentity) {
|
|
174
|
+
const senders = new Set();
|
|
175
|
+
for (const msg of chatMessages) {
|
|
176
|
+
if (msg.sender && msg.sender !== userIdentity) {
|
|
177
|
+
senders.add(msg.sender);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (senders.size === 1) return [...senders][0];
|
|
181
|
+
if (senders.size > 1) return [...senders].join(", ");
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function messageFromRecord(msg, cutoffMs) {
|
|
186
|
+
if (isSystemMessage(msg)) return null;
|
|
187
|
+
|
|
188
|
+
const arrivalTime = msg.originalArrivalTime ?? msg.clientArrivalTime ?? null;
|
|
189
|
+
const ts =
|
|
190
|
+
typeof arrivalTime === "string" ? parseInt(arrivalTime, 10) : arrivalTime;
|
|
191
|
+
|
|
192
|
+
if (ts && cutoffMs && ts < cutoffMs) return null;
|
|
193
|
+
|
|
194
|
+
const content = htmlToText(msg.content ?? "");
|
|
195
|
+
if (!content) return null;
|
|
196
|
+
|
|
197
|
+
const senderName = normalizeName(
|
|
198
|
+
msg.imDisplayName ??
|
|
199
|
+
msg.fromDisplayNameInToken ??
|
|
200
|
+
msg.prioritizeImDisplayName ??
|
|
201
|
+
"",
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
timestamp: ts,
|
|
206
|
+
sender: senderName,
|
|
207
|
+
content,
|
|
208
|
+
edited: !!msg.skypeeditedid,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function extractMessagesFromReplychains(replychains, conversationId, cutoffMs) {
|
|
213
|
+
const messages = [];
|
|
214
|
+
for (const rc of replychains) {
|
|
215
|
+
if (rc.conversationId !== conversationId) continue;
|
|
216
|
+
if (!rc.messageMap) continue;
|
|
217
|
+
|
|
218
|
+
for (const [, msg] of Object.entries(rc.messageMap)) {
|
|
219
|
+
const record = messageFromRecord(msg, cutoffMs);
|
|
220
|
+
if (record) messages.push(record);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
messages.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
|
|
225
|
+
return messages;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function writeChatMarkdown(
|
|
229
|
+
slug,
|
|
230
|
+
displayName,
|
|
231
|
+
chatType,
|
|
232
|
+
participants,
|
|
233
|
+
messages,
|
|
234
|
+
) {
|
|
235
|
+
const lines = [];
|
|
236
|
+
|
|
237
|
+
if (chatType === "group") {
|
|
238
|
+
lines.push(`# Chat: ${displayName} (group)`);
|
|
239
|
+
lines.push("");
|
|
240
|
+
lines.push("**Platform:** Microsoft Teams");
|
|
241
|
+
lines.push("**Type:** Group chat");
|
|
242
|
+
if (participants.length > 0) {
|
|
243
|
+
lines.push(`**Participants:** ${participants.join(", ")}`);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
lines.push(`# Chat with ${displayName}`);
|
|
247
|
+
lines.push("");
|
|
248
|
+
lines.push("**Platform:** Microsoft Teams");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
lines.push(`**Last Synced:** ${new Date().toISOString().slice(0, 10)}`);
|
|
252
|
+
lines.push("");
|
|
253
|
+
|
|
254
|
+
for (const msg of messages) {
|
|
255
|
+
lines.push("---");
|
|
256
|
+
lines.push("");
|
|
257
|
+
lines.push(`### ${msg.sender || "Unknown"}`);
|
|
258
|
+
lines.push(`**Date:** ${formatTimestamp(msg.timestamp)}`);
|
|
259
|
+
if (msg.edited) lines.push("**Edited**");
|
|
260
|
+
lines.push("");
|
|
261
|
+
lines.push(msg.content);
|
|
262
|
+
lines.push("");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
writeFileSync(join(OUTDIR, `${slug}.md`), lines.join("\n"));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function loadUserIdentity() {
|
|
269
|
+
try {
|
|
270
|
+
const userMd = readFileSync(join(process.cwd(), "USER.md"), "utf-8");
|
|
271
|
+
const nameMatch = userMd.match(/\*\*Name:\*\*\s*(.+)/);
|
|
272
|
+
if (nameMatch) return nameMatch[1].trim();
|
|
273
|
+
} catch {
|
|
274
|
+
// USER.md not found or not readable
|
|
275
|
+
}
|
|
276
|
+
return "";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function processConversation(conv, replychains, cutoffMs, userIdentity) {
|
|
280
|
+
const convId = conv.id;
|
|
281
|
+
if (!convId) return null;
|
|
282
|
+
|
|
283
|
+
const chatType = is1to1Chat(conv)
|
|
284
|
+
? "1to1"
|
|
285
|
+
: isGroupChat(conv)
|
|
286
|
+
? "group"
|
|
287
|
+
: null;
|
|
288
|
+
if (!chatType) return null;
|
|
289
|
+
|
|
290
|
+
const chatMessages = extractMessagesFromReplychains(
|
|
291
|
+
replychains,
|
|
292
|
+
convId,
|
|
293
|
+
cutoffMs,
|
|
294
|
+
);
|
|
295
|
+
if (chatMessages.length === 0) return null;
|
|
296
|
+
|
|
297
|
+
let displayName = getChatDisplayName(conv);
|
|
298
|
+
if (!displayName) {
|
|
299
|
+
displayName = getChatDisplayNameFromMessages(chatMessages, userIdentity);
|
|
300
|
+
}
|
|
301
|
+
if (!displayName) return null;
|
|
302
|
+
|
|
303
|
+
const slug = toSlug(displayName);
|
|
304
|
+
if (!slug) return null;
|
|
305
|
+
|
|
306
|
+
const participants =
|
|
307
|
+
chatType === "group" ? extractParticipantsFromMessages(chatMessages) : [];
|
|
308
|
+
|
|
309
|
+
writeChatMarkdown(slug, displayName, chatType, participants, chatMessages);
|
|
310
|
+
|
|
311
|
+
const lastMsgTime = chatMessages[chatMessages.length - 1]?.timestamp;
|
|
312
|
+
const lastMsgIso = lastMsgTime
|
|
313
|
+
? new Date(lastMsgTime).toISOString().slice(0, 19)
|
|
314
|
+
: "";
|
|
315
|
+
|
|
316
|
+
return `${slug}\t${displayName}\t${lastMsgIso}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function main() {
|
|
320
|
+
let daysBack = 30;
|
|
321
|
+
const daysIdx = process.argv.indexOf("--days");
|
|
322
|
+
if (daysIdx !== -1 && process.argv[daysIdx + 1]) {
|
|
323
|
+
daysBack = parseInt(process.argv[daysIdx + 1], 10);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!existsSync(TEAMS_IDB_DIR)) {
|
|
327
|
+
console.error(
|
|
328
|
+
"Error: Teams IndexedDB not found. Is the Teams desktop app installed?",
|
|
329
|
+
);
|
|
330
|
+
console.error(`Expected: ${TEAMS_IDB_DIR}`);
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
mkdirSync(OUTDIR, { recursive: true });
|
|
335
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
336
|
+
|
|
337
|
+
const cutoffMs = Date.now() - daysBack * 86400000;
|
|
338
|
+
|
|
339
|
+
console.log(`Reading Teams IndexedDB from disk...`);
|
|
340
|
+
const startTime = Date.now();
|
|
341
|
+
const { conversations, messages: replychains } = readIndexedDb(TEAMS_IDB_DIR);
|
|
342
|
+
const readTime = Date.now() - startTime;
|
|
343
|
+
console.log(
|
|
344
|
+
`Read ${conversations.length} conversations, ${replychains.length} message chains in ${readTime}ms`,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const userIdentity = loadUserIdentity();
|
|
348
|
+
|
|
349
|
+
const indexLines = [];
|
|
350
|
+
let written = 0;
|
|
351
|
+
|
|
352
|
+
for (const conv of conversations) {
|
|
353
|
+
const indexLine = processConversation(
|
|
354
|
+
conv,
|
|
355
|
+
replychains,
|
|
356
|
+
cutoffMs,
|
|
357
|
+
userIdentity,
|
|
358
|
+
);
|
|
359
|
+
if (indexLine) {
|
|
360
|
+
indexLines.push(indexLine);
|
|
361
|
+
written++;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
indexLines.sort((a, b) => {
|
|
366
|
+
const tsA = a.split("\t")[2] ?? "";
|
|
367
|
+
const tsB = b.split("\t")[2] ?? "";
|
|
368
|
+
return tsB.localeCompare(tsA);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
writeFileSync(INDEX_FILE, indexLines.join("\n") + "\n");
|
|
372
|
+
writeFileSync(SYNC_STATE_FILE, new Date().toISOString());
|
|
373
|
+
|
|
374
|
+
console.log("\nTeams Sync Complete");
|
|
375
|
+
console.log(`Chats written: ${written}`);
|
|
376
|
+
console.log(`Messages within last ${daysBack} days`);
|
|
377
|
+
console.log(`Read time: ${readTime}ms`);
|
|
378
|
+
console.log(`Output: ${OUTDIR}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
main();
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: upstream-skill
|
|
3
|
+
description: Track changes made to skills in this installation and produce a changelog that can be included upstream. Use when skills have been modified, added, or removed locally and those changes should be contributed back to the monorepo.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Upstream
|
|
7
|
+
|
|
8
|
+
Track changes to skills in this installation and produce a structured changelog
|
|
9
|
+
so improvements can be contributed back to the upstream monorepo.
|
|
10
|
+
|
|
11
|
+
## Trigger
|
|
12
|
+
|
|
13
|
+
- The user asks to prepare local skill changes for upstream contribution.
|
|
14
|
+
- Skills in `.claude/skills/` have been modified, added, or removed.
|
|
15
|
+
- The user wants to document what changed locally before syncing upstream.
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- A working Outpost installation with `.claude/skills/`.
|
|
20
|
+
- Git available for change detection.
|
|
21
|
+
|
|
22
|
+
## Inputs
|
|
23
|
+
|
|
24
|
+
- `.claude/skills/*/SKILL.md` — current skill files.
|
|
25
|
+
- Git history — change detection.
|
|
26
|
+
|
|
27
|
+
## Outputs
|
|
28
|
+
|
|
29
|
+
- `.claude/skills/<skill>/CHANGELOG.md` — one per modified skill,
|
|
30
|
+
reverse-chronological.
|
|
31
|
+
|
|
32
|
+
<do_confirm_checklist goal="Verify changelogs are upstream-ready">
|
|
33
|
+
|
|
34
|
+
- [ ] Every changed skill has a `CHANGELOG.md` in its directory.
|
|
35
|
+
- [ ] Each entry has **What**, **Why**, and **Details** sections.
|
|
36
|
+
- [ ] Descriptions are specific enough for an upstream maintainer to act on (not
|
|
37
|
+
"updated SKILL.md" / "fixed stuff").
|
|
38
|
+
- [ ] New skills include a brief description of their purpose.
|
|
39
|
+
- [ ] Removed skills explain why they were removed.
|
|
40
|
+
- [ ] Dates come from git history, not guessed.
|
|
41
|
+
- [ ] No duplicate entries for the same change.
|
|
42
|
+
|
|
43
|
+
</do_confirm_checklist>
|
|
44
|
+
|
|
45
|
+
## Procedure
|
|
46
|
+
|
|
47
|
+
### 1. Identify changed skills
|
|
48
|
+
|
|
49
|
+
Use git to find skills added, modified, or deleted since the last changelog
|
|
50
|
+
entry (or since initial commit if no changelog exists):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Latest changelog date for a skill (if any)
|
|
54
|
+
head -20 .claude/skills/<skill>/CHANGELOG.md 2>/dev/null
|
|
55
|
+
|
|
56
|
+
# All changes touching skills
|
|
57
|
+
git log --oneline --name-status -- '.claude/skills/'
|
|
58
|
+
|
|
59
|
+
# Changes since last documented date for one skill
|
|
60
|
+
git log --after="<last-entry-date>" --name-status -- '.claude/skills/<skill>/'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If no changelog exists for a skill, consider all commits that touched that
|
|
64
|
+
skill's directory.
|
|
65
|
+
|
|
66
|
+
### 2. Classify each change
|
|
67
|
+
|
|
68
|
+
| Type | Description |
|
|
69
|
+
| ---------- | ---------------------------------------------------- |
|
|
70
|
+
| `added` | New skill that doesn't exist upstream |
|
|
71
|
+
| `modified` | Existing skill updated (workflow, checklists, tools) |
|
|
72
|
+
| `removed` | Skill deleted from the installation |
|
|
73
|
+
| `renamed` | Skill directory or file renamed |
|
|
74
|
+
|
|
75
|
+
For modifications, diff the current file against the prior version:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git diff HEAD~N -- '.claude/skills/<skill>/SKILL.md'
|
|
79
|
+
git diff <commit> -- '.claude/skills/<skill>/SKILL.md'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3. Describe each change
|
|
83
|
+
|
|
84
|
+
Every entry must answer:
|
|
85
|
+
|
|
86
|
+
1. **What changed?** — the specific section or behaviour modified.
|
|
87
|
+
2. **Why?** — the problem encountered or improvement discovered in use.
|
|
88
|
+
3. **How?** — a summary of the actual change (not a full diff).
|
|
89
|
+
|
|
90
|
+
Good: "Added a safety check to Step 3 — agents were skipping validation when the
|
|
91
|
+
source directory was empty, causing silent failures."
|
|
92
|
+
|
|
93
|
+
Bad: "Updated SKILL.md" / "Fixed stuff" / "Changed line 42".
|
|
94
|
+
|
|
95
|
+
### 4. Write the changelog
|
|
96
|
+
|
|
97
|
+
For each changed skill, create or update `.claude/skills/<skill>/CHANGELOG.md`
|
|
98
|
+
using this format (newest first):
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
# <skill> Changelog
|
|
102
|
+
|
|
103
|
+
Changes to this skill that should be considered for upstream inclusion in
|
|
104
|
+
the Forward Impact monorepo.
|
|
105
|
+
|
|
106
|
+
## <YYYY-MM-DD>
|
|
107
|
+
|
|
108
|
+
**What:** <one-line summary>
|
|
109
|
+
|
|
110
|
+
**Why:** <problem or improvement that motivated it>
|
|
111
|
+
|
|
112
|
+
**Details:**
|
|
113
|
+
<2–5 lines describing the specific changes>
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
For **new** skills, write a single `added` entry describing the skill's purpose.
|
|
119
|
+
For **removed** skills, the changelog is the last file remaining in the
|
|
120
|
+
directory and explains the removal.
|
|
121
|
+
|
|
122
|
+
Worked examples in [references/examples.md](references/examples.md).
|
|
123
|
+
|
|
124
|
+
## Notes
|
|
125
|
+
|
|
126
|
+
- This skill **documents only** — it does not push or merge anything.
|
|
127
|
+
- Per-skill changelogs are consumed by the **downstream** skill in the upstream
|
|
128
|
+
monorepo.
|
|
129
|
+
- When in doubt about whether a change is upstream-worthy, include it; the
|
|
130
|
+
upstream maintainer decides what to incorporate.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Changelog Examples
|
|
2
|
+
|
|
3
|
+
Reference output for `upstream-skill` Step 4. Each modified skill has its own
|
|
4
|
+
`CHANGELOG.md` with reverse-chronological entries.
|
|
5
|
+
|
|
6
|
+
## Modified skill
|
|
7
|
+
|
|
8
|
+
`.claude/skills/req-track/CHANGELOG.md`:
|
|
9
|
+
|
|
10
|
+
```markdown
|
|
11
|
+
# req-track Changelog
|
|
12
|
+
|
|
13
|
+
Changes to this skill that should be considered for upstream inclusion in
|
|
14
|
+
the Forward Impact monorepo.
|
|
15
|
+
|
|
16
|
+
## 2026-03-01
|
|
17
|
+
|
|
18
|
+
**What:** Added gender field extraction for diversity tracking
|
|
19
|
+
|
|
20
|
+
**Why:** Recruitment pipeline lacked diversity metrics — pool composition
|
|
21
|
+
was invisible without structured gender data.
|
|
22
|
+
|
|
23
|
+
**Details:**
|
|
24
|
+
- Added Gender field to candidate brief template (Woman / Man / —)
|
|
25
|
+
- Added extraction rules: pronouns, gendered titles
|
|
26
|
+
- Added explicit note that the field has no bearing on hiring decisions
|
|
27
|
+
- Updated quality checklist to verify the gender field
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## New skill
|
|
33
|
+
|
|
34
|
+
`.claude/skills/hyprnote-process/CHANGELOG.md`:
|
|
35
|
+
|
|
36
|
+
```markdown
|
|
37
|
+
# hyprnote-process Changelog
|
|
38
|
+
|
|
39
|
+
Changes to this skill that should be considered for upstream inclusion in
|
|
40
|
+
the Forward Impact monorepo.
|
|
41
|
+
|
|
42
|
+
## 2026-03-01
|
|
43
|
+
|
|
44
|
+
**What:** New skill for processing Hyprnote meeting recordings
|
|
45
|
+
|
|
46
|
+
**Why:** Meeting notes were being lost — Hyprnote captures transcriptions
|
|
47
|
+
but they weren't being integrated into the knowledge base.
|
|
48
|
+
|
|
49
|
+
**Details:**
|
|
50
|
+
- Reads transcription files from `~/.cache/fit/outpost/hyprnote/`
|
|
51
|
+
- Extracts people, decisions, and action items
|
|
52
|
+
- Creates meeting notes in `knowledge/Meetings/`
|
|
53
|
+
- Links attendees to `knowledge/People/` entries
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Tweak to existing skill
|
|
59
|
+
|
|
60
|
+
`.claude/skills/extract-entities/CHANGELOG.md`:
|
|
61
|
+
|
|
62
|
+
```markdown
|
|
63
|
+
# extract-entities Changelog
|
|
64
|
+
|
|
65
|
+
Changes to this skill that should be considered for upstream inclusion in
|
|
66
|
+
the Forward Impact monorepo.
|
|
67
|
+
|
|
68
|
+
## 2026-02-15
|
|
69
|
+
|
|
70
|
+
**What:** Increased batch size from 5 to 10 files per run
|
|
71
|
+
|
|
72
|
+
**Why:** Processing was too slow for large inboxes — 5 files per batch
|
|
73
|
+
meant dozens of runs to catch up after a week of email.
|
|
74
|
+
|
|
75
|
+
**Details:**
|
|
76
|
+
- Changed batch size constant from 5 to 10 in Step 1
|
|
77
|
+
- Added a note about context window limits for batches > 15
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
```
|