@forwardimpact/basecamp 2.5.0 → 2.6.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.
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Query synced Apple Calendar events by date/time window.
4
+ *
5
+ * Reads JSON event files from ~/.cache/fit/basecamp/apple_calendar/ and filters
6
+ * them by date range, time window, or upcoming interval. Designed to eliminate
7
+ * the need for agents to write bespoke calendar-parsing scripts.
8
+ *
9
+ * Usage:
10
+ * node scripts/query.mjs --today Today's events
11
+ * node scripts/query.mjs --tomorrow Tomorrow's events
12
+ * node scripts/query.mjs --upcoming 2h Events in the next 2 hours
13
+ * node scripts/query.mjs --date 2026-03-09 Events on a specific date
14
+ * node scripts/query.mjs --range 2026-03-09 2026-03-11 Events in date range
15
+ * node scripts/query.mjs --today --tomorrow Combine multiple filters
16
+ * node scripts/query.mjs --json Output as JSON (default: table)
17
+ * node scripts/query.mjs --include-all-day Include all-day events
18
+ */
19
+
20
+ import { readFileSync, readdirSync } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { homedir } from "node:os";
23
+
24
+ const HOME = homedir();
25
+ const CAL_DIR = join(HOME, ".cache/fit/basecamp/apple_calendar");
26
+
27
+ if (process.argv.includes("-h") || process.argv.includes("--help")) {
28
+ console.log(`query — filter synced calendar events by date/time
29
+
30
+ Usage:
31
+ node scripts/query.mjs [options]
32
+
33
+ Time filters (combinable):
34
+ --today Events starting today
35
+ --tomorrow Events starting tomorrow
36
+ --upcoming <interval> Events starting within interval (e.g., 2h, 30m, 1d)
37
+ --date <YYYY-MM-DD> Events on a specific date
38
+ --range <start> <end> Events between two dates (inclusive)
39
+
40
+ Output options:
41
+ --json Output as JSON array (default: formatted table)
42
+ --include-all-day Include all-day events (excluded by default)
43
+ --no-attendees Omit attendee names from output
44
+
45
+ Defaults to --today if no time filter is specified.`);
46
+ process.exit(0);
47
+ }
48
+
49
+ // --- Parse arguments ---
50
+
51
+ const args = process.argv.slice(2);
52
+ const flags = {
53
+ today: args.includes("--today"),
54
+ tomorrow: args.includes("--tomorrow"),
55
+ json: args.includes("--json"),
56
+ includeAllDay: args.includes("--include-all-day"),
57
+ noAttendees: args.includes("--no-attendees"),
58
+ upcoming: null,
59
+ date: null,
60
+ rangeStart: null,
61
+ rangeEnd: null,
62
+ };
63
+
64
+ const upcomingIdx = args.indexOf("--upcoming");
65
+ if (upcomingIdx !== -1 && args[upcomingIdx + 1]) {
66
+ flags.upcoming = parseInterval(args[upcomingIdx + 1]);
67
+ }
68
+
69
+ const dateIdx = args.indexOf("--date");
70
+ if (dateIdx !== -1 && args[dateIdx + 1]) {
71
+ flags.date = args[dateIdx + 1];
72
+ }
73
+
74
+ const rangeIdx = args.indexOf("--range");
75
+ if (rangeIdx !== -1 && args[rangeIdx + 1] && args[rangeIdx + 2]) {
76
+ flags.rangeStart = args[rangeIdx + 1];
77
+ flags.rangeEnd = args[rangeIdx + 2];
78
+ }
79
+
80
+ // Default to --today if no time filter specified
81
+ if (
82
+ !flags.today &&
83
+ !flags.tomorrow &&
84
+ !flags.upcoming &&
85
+ !flags.date &&
86
+ !flags.rangeStart
87
+ ) {
88
+ flags.today = true;
89
+ }
90
+
91
+ /**
92
+ * Parse an interval string like "2h", "30m", "1d" into milliseconds.
93
+ */
94
+ function parseInterval(str) {
95
+ const match = str.match(/^(\d+(?:\.\d+)?)\s*(m|min|h|hr|d)$/i);
96
+ if (!match) {
97
+ console.error(`Invalid interval: ${str}. Use e.g., 2h, 30m, 1d`);
98
+ process.exit(1);
99
+ }
100
+ const n = parseFloat(match[1]);
101
+ const unit = match[2].toLowerCase();
102
+ if (unit === "m" || unit === "min") return n * 60 * 1000;
103
+ if (unit === "h" || unit === "hr") return n * 60 * 60 * 1000;
104
+ if (unit === "d") return n * 24 * 60 * 60 * 1000;
105
+ return n * 60 * 60 * 1000; // default hours
106
+ }
107
+
108
+ /**
109
+ * Get the start of a day in local time.
110
+ */
111
+ function dayStart(date) {
112
+ const d = new Date(date);
113
+ d.setHours(0, 0, 0, 0);
114
+ return d;
115
+ }
116
+
117
+ /**
118
+ * Get the end of a day in local time.
119
+ */
120
+ function dayEnd(date) {
121
+ const d = new Date(date);
122
+ d.setHours(23, 59, 59, 999);
123
+ return d;
124
+ }
125
+
126
+ // --- Load events ---
127
+
128
+ let files;
129
+ try {
130
+ files = readdirSync(CAL_DIR).filter((f) => f.endsWith(".json"));
131
+ } catch {
132
+ console.error(
133
+ `Calendar directory not found: ${CAL_DIR}\nRun sync first: node scripts/sync.mjs`,
134
+ );
135
+ process.exit(1);
136
+ }
137
+
138
+ const events = [];
139
+ for (const f of files) {
140
+ try {
141
+ const ev = JSON.parse(readFileSync(join(CAL_DIR, f), "utf8"));
142
+
143
+ // Skip all-day events unless requested
144
+ if (ev.allDay && !flags.includeAllDay) continue;
145
+
146
+ const startStr = ev.start?.dateTime || ev.start?.date;
147
+ if (!startStr) continue;
148
+
149
+ const startDt = new Date(startStr);
150
+ if (isNaN(startDt.getTime())) continue;
151
+
152
+ const endStr = ev.end?.dateTime || ev.end?.date;
153
+ const endDt = endStr ? new Date(endStr) : startDt;
154
+
155
+ events.push({
156
+ summary: ev.summary || "(untitled)",
157
+ start: startDt,
158
+ end: endDt,
159
+ startIso: startStr,
160
+ endIso: endStr || startStr,
161
+ timeZone: ev.start?.timeZone || null,
162
+ allDay: Boolean(ev.allDay),
163
+ location: ev.location || null,
164
+ conferenceUrl: ev.conferenceUrl || null,
165
+ calendar: ev.calendar || null,
166
+ organizer: ev.organizer || null,
167
+ attendees: (ev.attendees || [])
168
+ .filter((a) => !a.self)
169
+ .map((a) => ({ name: a.name || null, email: a.email })),
170
+ });
171
+ } catch {
172
+ // Skip malformed files
173
+ }
174
+ }
175
+
176
+ events.sort((a, b) => a.start - b.start);
177
+
178
+ // --- Filter events ---
179
+
180
+ const now = new Date();
181
+ const today = dayStart(now);
182
+ const todayE = dayEnd(now);
183
+ const tomorrow = dayStart(new Date(now.getTime() + 86400000));
184
+ const tomorrowE = dayEnd(new Date(now.getTime() + 86400000));
185
+
186
+ const filtered = events.filter((ev) => {
187
+ let match = false;
188
+
189
+ if (flags.today) {
190
+ match = match || (ev.start >= today && ev.start <= todayE);
191
+ }
192
+
193
+ if (flags.tomorrow) {
194
+ match = match || (ev.start >= tomorrow && ev.start <= tomorrowE);
195
+ }
196
+
197
+ if (flags.upcoming) {
198
+ const cutoff = new Date(now.getTime() + flags.upcoming);
199
+ match = match || (ev.start >= now && ev.start <= cutoff);
200
+ }
201
+
202
+ if (flags.date) {
203
+ const dStart = dayStart(flags.date + "T00:00:00");
204
+ const dEnd = dayEnd(flags.date + "T00:00:00");
205
+ match = match || (ev.start >= dStart && ev.start <= dEnd);
206
+ }
207
+
208
+ if (flags.rangeStart && flags.rangeEnd) {
209
+ const rStart = dayStart(flags.rangeStart + "T00:00:00");
210
+ const rEnd = dayEnd(flags.rangeEnd + "T00:00:00");
211
+ match = match || (ev.start >= rStart && ev.start <= rEnd);
212
+ }
213
+
214
+ return match;
215
+ });
216
+
217
+ // --- Output ---
218
+
219
+ if (flags.json) {
220
+ const output = filtered.map((ev) => {
221
+ const obj = {
222
+ summary: ev.summary,
223
+ start: ev.startIso,
224
+ end: ev.endIso,
225
+ timeZone: ev.timeZone,
226
+ allDay: ev.allDay,
227
+ location: ev.location,
228
+ conferenceUrl: ev.conferenceUrl,
229
+ calendar: ev.calendar,
230
+ organizer: ev.organizer,
231
+ };
232
+ if (!flags.noAttendees) {
233
+ obj.attendees = ev.attendees;
234
+ }
235
+ return obj;
236
+ });
237
+ console.log(JSON.stringify(output, null, 2));
238
+ } else {
239
+ // Formatted table output
240
+ if (filtered.length === 0) {
241
+ console.log("No events found.");
242
+ process.exit(0);
243
+ }
244
+
245
+ // Group by date
246
+ const byDate = new Map();
247
+ for (const ev of filtered) {
248
+ const dateKey = ev.start.toLocaleDateString("sv-SE"); // YYYY-MM-DD
249
+ if (!byDate.has(dateKey)) byDate.set(dateKey, []);
250
+ byDate.get(dateKey).push(ev);
251
+ }
252
+
253
+ for (const [date, dayEvents] of byDate) {
254
+ const isToday = date === now.toLocaleDateString("sv-SE");
255
+ const isTomorrow =
256
+ date === new Date(now.getTime() + 86400000).toLocaleDateString("sv-SE");
257
+ const label = isToday ? " (today)" : isTomorrow ? " (tomorrow)" : "";
258
+ console.log(`\n=== ${date}${label} ===`);
259
+
260
+ for (const ev of dayEvents) {
261
+ const tz =
262
+ ev.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone;
263
+ const time = ev.allDay
264
+ ? "all-day"
265
+ : ev.start.toLocaleTimeString("en-GB", {
266
+ hour: "2-digit",
267
+ minute: "2-digit",
268
+ timeZone: tz,
269
+ });
270
+ const endTime = ev.allDay
271
+ ? ""
272
+ : "-" +
273
+ ev.end.toLocaleTimeString("en-GB", {
274
+ hour: "2-digit",
275
+ minute: "2-digit",
276
+ timeZone: tz,
277
+ });
278
+
279
+ // Minutes until start (only for future events)
280
+ let countdown = "";
281
+ if (ev.start > now && flags.upcoming) {
282
+ const mins = Math.round((ev.start - now) / 60000);
283
+ countdown = ` [in ${mins}min]`;
284
+ }
285
+
286
+ const parts = [`${time}${endTime}`, ev.summary];
287
+
288
+ if (!flags.noAttendees && ev.attendees.length > 0) {
289
+ const names = ev.attendees
290
+ .slice(0, 8)
291
+ .map((a) => a.name || a.email)
292
+ .join(", ");
293
+ parts.push(names);
294
+ }
295
+
296
+ console.log(parts.join(" | ") + countdown);
297
+ }
298
+ }
299
+
300
+ console.log(`\n${filtered.length} event(s) | now: ${now.toISOString()}`);
301
+ }
@@ -0,0 +1,296 @@
1
+ ---
2
+ name: synthesize-deck
3
+ description: Synthesize PowerPoint decks into engineer-friendly markdown briefs covering Jobs-To-Be-Done, dependencies, and synthetic data needs. Use when the user asks to break down, summarize, or make sense of a slide deck (.pptx) for engineering work.
4
+ compatibility: Node.js only — no external dependencies.
5
+ ---
6
+
7
+ # Synthesize Deck
8
+
9
+ Turn messy PowerPoint specification decks into clear, actionable markdown briefs
10
+ that forward deployed engineers can actually build from. Strips out business
11
+ jargon and focuses on what matters: what needs to be built, what blocks
12
+ progress, and what data you need to start prototyping.
13
+
14
+ ## Trigger
15
+
16
+ Run when the user asks to:
17
+
18
+ - Summarize, synthesize, or break down a `.pptx` deck
19
+ - Make sense of a specification or proposal deck for engineering
20
+ - Create an engineering brief from a slide deck
21
+ - Understand what a project deck is actually asking for
22
+
23
+ ## Prerequisites
24
+
25
+ - Node.js 18+
26
+ - Input files must be `.pptx` format
27
+
28
+ ## Inputs
29
+
30
+ - One or more `.pptx` file paths
31
+ - Optional: specific focus areas the engineer cares about
32
+
33
+ ## Outputs
34
+
35
+ - One markdown file per deck (or one combined file for related decks) written to
36
+ `knowledge/Projects/` with the naming pattern
37
+ `{Project Name} - Engineering Brief.md`
38
+
39
+ ---
40
+
41
+ ## Workflow
42
+
43
+ ### Step 1: Extract Text from PPTX
44
+
45
+ Use the extraction script to pull all slide text from the deck:
46
+
47
+ ```bash
48
+ node .claude/skills/synthesize-deck/scripts/extract-pptx.mjs "$FILE_PATH"
49
+ ```
50
+
51
+ For multiple decks, pass all files at once:
52
+
53
+ ```bash
54
+ node .claude/skills/synthesize-deck/scripts/extract-pptx.mjs deck1.pptx deck2.pptx
55
+ ```
56
+
57
+ To save the extracted text for analysis:
58
+
59
+ ```bash
60
+ node .claude/skills/synthesize-deck/scripts/extract-pptx.mjs "$FILE_PATH" -o /tmp/deck_extract.txt
61
+ ```
62
+
63
+ Read the full extracted text before proceeding.
64
+
65
+ ### Step 2: Identify the Core Problem
66
+
67
+ Read through all slides and answer:
68
+
69
+ - What process or workflow exists today?
70
+ - What is broken, slow, or painful about it?
71
+ - Who suffers from these problems (the actual humans doing the work)?
72
+
73
+ Write this up in plain language. Avoid repeating the deck's marketing framing.
74
+ Describe the problem like you're explaining it to a teammate over coffee.
75
+
76
+ ### Step 3: Extract Jobs-To-Be-Done
77
+
78
+ JTBD captures what users actually need to accomplish. For each distinct user
79
+ role identified in the deck, extract jobs using this format:
80
+
81
+ ```
82
+ When [situation], I need to [action], so that [outcome].
83
+ ```
84
+
85
+ **Rules for good JTBDs:**
86
+
87
+ - Focus on the user's goal, not the proposed solution
88
+ - Use concrete, specific language (not "leverage AI to optimize")
89
+ - One job per statement — don't combine multiple needs
90
+ - Include the "so that" — the outcome matters for prioritization
91
+ - Group by user role/persona
92
+
93
+ **Common traps to avoid:**
94
+
95
+ - Don't just restate the deck's feature list as jobs
96
+ - Don't use the deck's jargon ("orchestration engine", "intelligence hub")
97
+ - A job should make sense even if you threw away the proposed solution
98
+
99
+ ### Step 4: Map Dependencies
100
+
101
+ Engineers need to know what blocks them before they can build. Extract three
102
+ categories:
103
+
104
+ #### 4a. Data Dependencies
105
+
106
+ For each data source mentioned or implied:
107
+
108
+ | Data | Where It Lives | Format | Access | Blocker? |
109
+ | ----------- | -------------------------- | ---------------------------------- | ------------------------------- | -------------- |
110
+ | _What data_ | _System/team that owns it_ | _Structured/unstructured/API/file_ | _Do we have it? Can we get it?_ | _Yes/No + why_ |
111
+
112
+ Flag any data that is:
113
+
114
+ - Mentioned but doesn't seem to exist yet
115
+ - Locked behind systems the team may not have access to
116
+ - Unstructured and would need significant preprocessing
117
+ - Subject to compliance/privacy constraints
118
+
119
+ #### 4b. System Dependencies
120
+
121
+ List every external system, API, or platform the solution needs to integrate
122
+ with. For each, note:
123
+
124
+ - What the integration does
125
+ - Whether it's read-only or read-write
126
+ - Whether an API exists or if it's manual/scraping
127
+ - Whether access has been confirmed
128
+
129
+ #### 4c. People Dependencies
130
+
131
+ List approvals, reviews, or co-creation required from specific roles or teams
132
+ before engineering work can proceed. Flag anything that looks like a long
133
+ lead-time dependency (legal review, compliance sign-off, vendor contracts).
134
+
135
+ ### Step 5: Define Synthetic Data Needs
136
+
137
+ Engineers need data to prototype before real data pipelines exist. For each core
138
+ feature or use case, specify:
139
+
140
+ **What to generate:**
141
+
142
+ - The data entity (e.g., "copay claims", "enrollment forms", "contract
143
+ documents")
144
+ - Key fields and their types
145
+ - Realistic value ranges and distributions
146
+ - Edge cases that matter (e.g., incomplete forms, anomalous claims)
147
+ - Volume needed for meaningful testing
148
+
149
+ **What to simulate:**
150
+
151
+ - Workflows and state transitions (e.g., case moving from intake to BV to PA)
152
+ - Time-series patterns (e.g., weekly claims with seasonal variation)
153
+ - Multi-actor interactions (e.g., patient submits, specialist reviews, payer
154
+ responds)
155
+ - Error conditions and failure modes
156
+
157
+ **Format guidance:**
158
+
159
+ - Prefer CSV/JSON for structured data
160
+ - Include realistic PII-shaped data (fake names, addresses, IDs) — never real
161
+ PII
162
+ - Include both happy-path and adversarial examples
163
+ - Consider what data would be needed to train/evaluate ML models
164
+
165
+ ### Step 6: Summarize the Proposed Solution
166
+
167
+ Describe what the deck is proposing to build, but translate it into engineering
168
+ terms:
169
+
170
+ - What are the core system components?
171
+ - What does each component actually do (in plain terms)?
172
+ - How do they connect to each other?
173
+ - What is the data flow end-to-end?
174
+ - What AI/ML capabilities are needed and what are they actually doing?
175
+
176
+ Avoid just listing the deck's branded feature names. Translate:
177
+
178
+ - "Intelligent Intake Hub" → "OCR + NLP pipeline that extracts structured fields
179
+ from scanned enrollment forms"
180
+ - "Case Intelligence Hub" → "Dashboard pulling case status from CRM + ML risk
181
+ score for each active case"
182
+ - "Copay Guardian" → "Anomaly detection model on weekly claims data that flags
183
+ unusual patterns"
184
+
185
+ ### Step 7: Identify What's Missing
186
+
187
+ Call out gaps an engineer would notice:
188
+
189
+ - Features described without clear data sources
190
+ - AI capabilities mentioned without training data strategy
191
+ - Integrations assumed but not detailed
192
+ - User workflows that skip important error/edge cases
193
+ - Metrics promised without measurement infrastructure
194
+ - Timeline vs. scope mismatches
195
+
196
+ ### Step 8: Write the Brief
197
+
198
+ Assemble into a single markdown document with this structure:
199
+
200
+ ```markdown
201
+ ---
202
+ project: {Project Name}
203
+ source: {list of deck filenames}
204
+ date_synthesized: {today's date}
205
+ status: engineering-brief
206
+ ---
207
+
208
+ # {Project Name} — Engineering Brief
209
+
210
+ > One-paragraph plain-English summary of what this project is and why it exists.
211
+
212
+ ## The Problem
213
+
214
+ {Step 2 output — what's broken, who it affects, why it matters}
215
+
216
+ ## Jobs-To-Be-Done
217
+
218
+ ### {Persona 1}
219
+ - When [situation], I need to [action], so that [outcome].
220
+ - ...
221
+
222
+ ### {Persona 2}
223
+ - ...
224
+
225
+ ## What They Want to Build
226
+
227
+ {Step 6 output — solution in engineering terms, components, data flow}
228
+
229
+ ### System Architecture (Simplified)
230
+
231
+ {Text-based diagram or description of how components connect}
232
+
233
+ ### AI/ML Components
234
+
235
+ {What models/capabilities are needed, what they do, what data they need}
236
+
237
+ ## Dependencies
238
+
239
+ ### Data
240
+ {Step 4a table}
241
+
242
+ ### Systems & Integrations
243
+ {Step 4b list}
244
+
245
+ ### People & Approvals
246
+ {Step 4c list}
247
+
248
+ ## Synthetic Data for Prototyping
249
+
250
+ ### {Feature/Use Case 1}
251
+ {Step 5 output}
252
+
253
+ ### {Feature/Use Case 2}
254
+ {Step 5 output}
255
+
256
+ ## Gaps & Open Questions
257
+
258
+ {Step 7 output as a numbered list}
259
+
260
+ ## Phasing
261
+
262
+ {Timeline and wave structure from the deck, with engineering commentary on
263
+ what's realistic and what depends on what}
264
+
265
+ ## Key Metrics
266
+
267
+ {What success looks like, translated into measurable engineering terms}
268
+ ```
269
+
270
+ ### Step 9: Save and Report
271
+
272
+ 1. Write the brief to `knowledge/Projects/{Project Name} - Engineering Brief.md`
273
+ 2. Tell the user where the file is and give a 3-sentence summary of the project
274
+
275
+ ## Writing Style
276
+
277
+ - **Plain language.** Write like you're briefing a smart engineer who has zero
278
+ context on this project. No "synergize", no "orchestrate", no "leverage".
279
+ - **Concrete over abstract.** "Parse PDF enrollment forms into structured JSON"
280
+ beats "Intelligent document processing capability".
281
+ - **Honest about uncertainty.** If the deck is vague about something important,
282
+ say so. Don't fill gaps with assumptions.
283
+ - **Opinionated where helpful.** If a dependency looks like it will block the
284
+ team for weeks, flag it clearly. If a timeline looks unrealistic given the
285
+ scope, say that.
286
+ - **Short sentences.** Engineers scan, they don't read essays.
287
+
288
+ ## Constraints
289
+
290
+ - Never invent requirements not present in the source deck
291
+ - Always flag when you're interpreting vs. directly extracting
292
+ - Keep the brief under 2000 lines — this is a summary, not a transcription
293
+ - Use the knowledge base for additional context about mentioned people, orgs, or
294
+ projects
295
+ - If multiple decks describe related projects, create one combined brief with
296
+ clear sections per project and a shared dependencies section