@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,80 @@
|
|
|
1
|
+
# Apple Calendar Database Schema
|
|
2
|
+
|
|
3
|
+
The Apple Calendar SQLite database uses Core Data. Key tables and their actual
|
|
4
|
+
column names (verified on macOS Sonoma+).
|
|
5
|
+
|
|
6
|
+
## CalendarItem (events and reminders)
|
|
7
|
+
|
|
8
|
+
| Column | Type | Notes |
|
|
9
|
+
| ---------------- | ------- | ---------------------------------------------------------- |
|
|
10
|
+
| `ROWID` | INTEGER | Primary key |
|
|
11
|
+
| `summary` | TEXT | Event title |
|
|
12
|
+
| `start_date` | REAL | Core Data timestamp (seconds since 2001-01-01 UTC) |
|
|
13
|
+
| `end_date` | REAL | Core Data timestamp (null for all-day events) |
|
|
14
|
+
| `start_tz` | TEXT | IANA timezone (e.g., `Europe/Paris`), `_float` for all-day |
|
|
15
|
+
| `end_tz` | TEXT | IANA timezone, `_float` for all-day |
|
|
16
|
+
| `all_day` | INTEGER | 1 = all-day event |
|
|
17
|
+
| `location_id` | INTEGER | FK → Location.ROWID |
|
|
18
|
+
| `description` | TEXT | Event notes/body |
|
|
19
|
+
| `organizer_id` | INTEGER | FK → Identity.ROWID |
|
|
20
|
+
| `calendar_id` | INTEGER | FK → Calendar.ROWID |
|
|
21
|
+
| `has_attendees` | INTEGER | 1 = event has attendees |
|
|
22
|
+
| `conference_url` | TEXT | Video call URL (often null — check description too) |
|
|
23
|
+
| `entity_type` | INTEGER | 2 = calendar event |
|
|
24
|
+
|
|
25
|
+
## Identity (organizer info)
|
|
26
|
+
|
|
27
|
+
| Column | Type | Notes |
|
|
28
|
+
| -------------- | ---- | ------------------------------------------------------------- |
|
|
29
|
+
| `display_name` | TEXT | Full name (e.g., `"Chen, Sarah"`) |
|
|
30
|
+
| `address` | TEXT | Email with `mailto:` prefix (e.g., `"mailto:sarah@acme.com"`) |
|
|
31
|
+
| `first_name` | TEXT | Usually null — `display_name` is the reliable field |
|
|
32
|
+
| `last_name` | TEXT | Usually null — `display_name` is the reliable field |
|
|
33
|
+
|
|
34
|
+
**IMPORTANT:** Identity does NOT have an `email` column. Use `address` and strip
|
|
35
|
+
the `mailto:` prefix. Use `display_name` for the name (not
|
|
36
|
+
`first_name`/`last_name`, which are typically null).
|
|
37
|
+
|
|
38
|
+
## Participant (attendees and organizer)
|
|
39
|
+
|
|
40
|
+
| Column | Type | Notes |
|
|
41
|
+
| ------------- | ------- | -------------------------------------------------- |
|
|
42
|
+
| `ROWID` | INTEGER | Primary key |
|
|
43
|
+
| `entity_type` | INTEGER | 7 = attendee, 8 = organizer |
|
|
44
|
+
| `owner_id` | INTEGER | FK → CalendarItem.ROWID |
|
|
45
|
+
| `identity_id` | INTEGER | FK → Identity.ROWID (for display_name lookup) |
|
|
46
|
+
| `email` | TEXT | Email address (no `mailto:` prefix) |
|
|
47
|
+
| `status` | INTEGER | EKParticipantStatus (see mapping below) |
|
|
48
|
+
| `role` | INTEGER | 0 = unknown, 1 = required, 2 = optional, 3 = chair |
|
|
49
|
+
| `is_self` | INTEGER | 1 = this is the calendar owner |
|
|
50
|
+
|
|
51
|
+
**IMPORTANT:** Participant does NOT have `display_name`, `first_name`, or
|
|
52
|
+
`last_name` columns. To get the attendee's name, JOIN with Identity via
|
|
53
|
+
`identity_id`. There is NO `Attendee` table — only use `Participant`.
|
|
54
|
+
|
|
55
|
+
### EKParticipantStatus mapping
|
|
56
|
+
|
|
57
|
+
| Value | Status |
|
|
58
|
+
| ----- | ---------- |
|
|
59
|
+
| 0 | unknown |
|
|
60
|
+
| 1 | pending |
|
|
61
|
+
| 2 | accepted |
|
|
62
|
+
| 3 | declined |
|
|
63
|
+
| 4 | tentative |
|
|
64
|
+
| 5 | delegated |
|
|
65
|
+
| 6 | completed |
|
|
66
|
+
| 7 | in-process |
|
|
67
|
+
|
|
68
|
+
## Calendar (calendar metadata)
|
|
69
|
+
|
|
70
|
+
| Column | Type | Notes |
|
|
71
|
+
| ------- | ------- | ------------- |
|
|
72
|
+
| `ROWID` | INTEGER | Primary key |
|
|
73
|
+
| `title` | TEXT | Calendar name |
|
|
74
|
+
|
|
75
|
+
## Location
|
|
76
|
+
|
|
77
|
+
| Column | Type | Notes |
|
|
78
|
+
| ------- | ------- | --------------- |
|
|
79
|
+
| `ROWID` | INTEGER | Primary key |
|
|
80
|
+
| `title` | TEXT | Location string |
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Query synced Apple Calendar events by date/time window.
|
|
4
|
+
*
|
|
5
|
+
* Reads JSON event files from ~/.cache/fit/outpost/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/outpost/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
|
+
function inRange(dt, lo, hi) {
|
|
187
|
+
return dt >= lo && dt <= hi;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function matchesFilters(ev, f, ref) {
|
|
191
|
+
if (f.today && inRange(ev.start, ref.today, ref.todayE)) return true;
|
|
192
|
+
if (f.tomorrow && inRange(ev.start, ref.tomorrow, ref.tomorrowE)) return true;
|
|
193
|
+
if (f.upcoming) {
|
|
194
|
+
const cutoff = new Date(ref.now.getTime() + f.upcoming);
|
|
195
|
+
if (inRange(ev.start, ref.now, cutoff)) return true;
|
|
196
|
+
}
|
|
197
|
+
if (f.date) {
|
|
198
|
+
if (
|
|
199
|
+
inRange(
|
|
200
|
+
ev.start,
|
|
201
|
+
dayStart(f.date + "T00:00:00"),
|
|
202
|
+
dayEnd(f.date + "T00:00:00"),
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
if (f.rangeStart && f.rangeEnd) {
|
|
208
|
+
if (
|
|
209
|
+
inRange(
|
|
210
|
+
ev.start,
|
|
211
|
+
dayStart(f.rangeStart + "T00:00:00"),
|
|
212
|
+
dayEnd(f.rangeEnd + "T00:00:00"),
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const ref = { now, today, todayE, tomorrow, tomorrowE };
|
|
221
|
+
const filtered = events.filter((ev) => matchesFilters(ev, flags, ref));
|
|
222
|
+
|
|
223
|
+
// --- Output ---
|
|
224
|
+
|
|
225
|
+
if (flags.json) {
|
|
226
|
+
const output = filtered.map((ev) => {
|
|
227
|
+
const obj = {
|
|
228
|
+
summary: ev.summary,
|
|
229
|
+
start: ev.startIso,
|
|
230
|
+
end: ev.endIso,
|
|
231
|
+
timeZone: ev.timeZone,
|
|
232
|
+
allDay: ev.allDay,
|
|
233
|
+
location: ev.location,
|
|
234
|
+
conferenceUrl: ev.conferenceUrl,
|
|
235
|
+
calendar: ev.calendar,
|
|
236
|
+
organizer: ev.organizer,
|
|
237
|
+
};
|
|
238
|
+
if (!flags.noAttendees) {
|
|
239
|
+
obj.attendees = ev.attendees;
|
|
240
|
+
}
|
|
241
|
+
return obj;
|
|
242
|
+
});
|
|
243
|
+
console.log(JSON.stringify(output, null, 2));
|
|
244
|
+
} else {
|
|
245
|
+
// Formatted table output
|
|
246
|
+
if (filtered.length === 0) {
|
|
247
|
+
console.log("No events found.");
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Group by date
|
|
252
|
+
const byDate = new Map();
|
|
253
|
+
for (const ev of filtered) {
|
|
254
|
+
const dateKey = ev.start.toLocaleDateString("sv-SE"); // YYYY-MM-DD
|
|
255
|
+
if (!byDate.has(dateKey)) byDate.set(dateKey, []);
|
|
256
|
+
byDate.get(dateKey).push(ev);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const [date, dayEvents] of byDate) {
|
|
260
|
+
const isToday = date === now.toLocaleDateString("sv-SE");
|
|
261
|
+
const isTomorrow =
|
|
262
|
+
date === new Date(now.getTime() + 86400000).toLocaleDateString("sv-SE");
|
|
263
|
+
const label = isToday ? " (today)" : isTomorrow ? " (tomorrow)" : "";
|
|
264
|
+
console.log(`\n=== ${date}${label} ===`);
|
|
265
|
+
|
|
266
|
+
for (const ev of dayEvents) {
|
|
267
|
+
const tz =
|
|
268
|
+
ev.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
269
|
+
const time = ev.allDay
|
|
270
|
+
? "all-day"
|
|
271
|
+
: ev.start.toLocaleTimeString("en-GB", {
|
|
272
|
+
hour: "2-digit",
|
|
273
|
+
minute: "2-digit",
|
|
274
|
+
timeZone: tz,
|
|
275
|
+
});
|
|
276
|
+
const endTime = ev.allDay
|
|
277
|
+
? ""
|
|
278
|
+
: "-" +
|
|
279
|
+
ev.end.toLocaleTimeString("en-GB", {
|
|
280
|
+
hour: "2-digit",
|
|
281
|
+
minute: "2-digit",
|
|
282
|
+
timeZone: tz,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Minutes until start (only for future events)
|
|
286
|
+
let countdown = "";
|
|
287
|
+
if (ev.start > now && flags.upcoming) {
|
|
288
|
+
const mins = Math.round((ev.start - now) / 60000);
|
|
289
|
+
countdown = ` [in ${mins}min]`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const parts = [`${time}${endTime}`, ev.summary];
|
|
293
|
+
|
|
294
|
+
if (!flags.noAttendees && ev.attendees.length > 0) {
|
|
295
|
+
const names = ev.attendees
|
|
296
|
+
.slice(0, 8)
|
|
297
|
+
.map((a) => a.name || a.email)
|
|
298
|
+
.join(", ");
|
|
299
|
+
parts.push(names);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.log(parts.join(" | ") + countdown);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
console.log(`\n${filtered.length} event(s) | now: ${now.toISOString()}`);
|
|
307
|
+
}
|