@cayde-6/icalendar 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cayde-6
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,245 @@
1
+ # icalendar
2
+
3
+ [![CI](https://github.com/cayde-6/icalendar/actions/workflows/ci.yml/badge.svg)](https://github.com/cayde-6/icalendar/actions/workflows/ci.yml)
4
+ [![Release](https://github.com/cayde-6/icalendar/actions/workflows/release.yml/badge.svg)](https://github.com/cayde-6/icalendar/actions/workflows/release.yml)
5
+ [![Coverage](https://codecov.io/gh/cayde-6/icalendar/branch/main/graph/badge.svg)](https://codecov.io/gh/cayde-6/icalendar)
6
+
7
+ ![icalendar hero](./assets/hero.png)
8
+
9
+ Production-ready TypeScript CLI for **CalDAV calendars**, **iCalendar events**, and **agent-friendly automation**.
10
+
11
+ `icalendar` gives agents and scripts a thin, reliable interface for:
12
+ - listing calendars
13
+ - listing events
14
+ - creating events
15
+ - updating events
16
+ - deleting events
17
+ - sending attendee invites through CalDAV/iCalendar
18
+ - setting a friendly organizer display name (for example `Bender`)
19
+
20
+ It was validated live against **real iCloud CalDAV** with create → update → invite → delete flows.
21
+
22
+ ## Why this exists
23
+
24
+ Most CalDAV tooling is either too low-level for agents or too UI-centric for automation. This repo wraps the ugly parts behind a small CLI and a layered codebase that is easy to embed, extend, and reason about.
25
+
26
+ ## Features
27
+
28
+ - ESM TypeScript CLI with clean layering
29
+ - CalDAV access via `tsdav`
30
+ - invite-ready ICS generation
31
+ - attendee support for create/update flows
32
+ - configurable organizer common name via env
33
+ - text and JSON output modes
34
+ - runtime-safe handling of `--help` / `--version`
35
+ - live-tested against iCloud CalDAV
36
+
37
+ ## Install
38
+
39
+ ### From npm
40
+
41
+ ```bash
42
+ npm install -g @cayde-6/icalendar
43
+ icalendar --help
44
+ ```
45
+
46
+ ### From source
47
+
48
+ ```bash
49
+ git clone https://github.com/cayde-6/icalendar.git
50
+ cd icalendar
51
+ npm install
52
+ npm run build
53
+ ```
54
+
55
+ ### Use as a local CLI
56
+
57
+ ```bash
58
+ npm link
59
+ icalendar --help
60
+ ```
61
+
62
+ ### Use without linking
63
+
64
+ ```bash
65
+ node --import tsx src/cli.ts --help
66
+ node dist/cli.js calendars list
67
+ ```
68
+
69
+ ## Configuration
70
+
71
+ Copy the example file:
72
+
73
+ ```bash
74
+ cp .env.example .env
75
+ ```
76
+
77
+ Required configuration:
78
+
79
+ ```env
80
+ CALDAV_SERVER_URL=https://caldav.icloud.com/
81
+ CALDAV_USERNAME=primary.attendee@example.test
82
+ CALDAV_PASSWORD=app-specific-password
83
+ ```
84
+
85
+ Optional configuration:
86
+
87
+ ```env
88
+ CALDAV_CALENDAR_NAME=OpenClaw Test
89
+ CALDAV_ORGANIZER_NAME=Bender
90
+ CALDAV_RANGE_START=2026-05-07T00:00:00+02:00
91
+ CALDAV_RANGE_END=2026-05-08T00:00:00+02:00
92
+ CALDAV_EXPAND_RECURRING=true
93
+ ```
94
+
95
+ ### iCloud setup: generate an app-specific password
96
+
97
+ For iCloud CalDAV you need an **app-specific password**. Your normal Apple ID password will not work.
98
+
99
+ How to generate it:
100
+
101
+ 1. Go to your Apple account settings: <https://account.apple.com/>
102
+ 2. Sign in with the Apple account that owns the iCloud calendar
103
+ 3. Open the **Sign-In and Security** section
104
+ 4. Find **App-Specific Passwords**
105
+ 5. Create a new password, for example named `icalendar`
106
+ 6. Copy the generated password and put it into:
107
+
108
+ ```env
109
+ CALDAV_PASSWORD=your-app-specific-password
110
+ ```
111
+
112
+ Recommended iCloud values:
113
+
114
+ ```env
115
+ CALDAV_SERVER_URL=https://caldav.icloud.com/
116
+ CALDAV_USERNAME=your-apple-id-email@example.com
117
+ CALDAV_PASSWORD=your-app-specific-password
118
+ ```
119
+
120
+ If authentication fails, the most common cause is using the normal Apple ID password instead of the generated app-specific one.
121
+
122
+ ## Quick examples
123
+
124
+ ### List calendars
125
+
126
+ ```bash
127
+ icalendar calendars list
128
+ icalendar calendars list --json
129
+ ```
130
+
131
+ ### List events
132
+
133
+ ```bash
134
+ icalendar events list
135
+ icalendar events list --json
136
+ ```
137
+
138
+ ### Create an event
139
+
140
+ ```bash
141
+ icalendar events create \
142
+ --summary "Family sync" \
143
+ --start "2026-05-07T18:00:00+02:00" \
144
+ --end "2026-05-07T18:30:00+02:00" \
145
+ --description "Agenda review" \
146
+ --location "Belgrade" \
147
+ --attendees "primary.attendee@example.test,secondary.attendee@example.test"
148
+ ```
149
+
150
+ ### Update an event
151
+
152
+ ```bash
153
+ icalendar events update \
154
+ --url "https://caldav.example.com/calendars/personal/event.ics" \
155
+ --summary "Family sync" \
156
+ --start "2026-05-07T18:00:00+02:00" \
157
+ --end "2026-05-07T18:45:00+02:00" \
158
+ --attendees "primary.attendee@example.test,secondary.attendee@example.test"
159
+ ```
160
+
161
+ ### Delete an event
162
+
163
+ ```bash
164
+ icalendar events delete "https://caldav.example.com/calendars/personal/event.ics"
165
+ ```
166
+
167
+ ## Agent integration
168
+
169
+ If another agent/session wants to adopt this CLI, start here:
170
+ - [Agent integration guide](./docs/agent-integration.md)
171
+ - [Architecture](./docs/architecture.md)
172
+
173
+ Short version:
174
+ 1. install dependencies
175
+ 2. provide `.env`
176
+ 3. run `npm run build`
177
+ 4. use `icalendar ...` or `node dist/cli.js ...`
178
+ 5. prefer `--json` for machine consumers
179
+
180
+ ## Development
181
+
182
+ ```bash
183
+ npm run check
184
+ npm run build
185
+ npm test
186
+ npm run test:coverage
187
+ npm run verify
188
+ ```
189
+
190
+ ## Test strategy
191
+
192
+ Current coverage includes:
193
+ - calendar selection rules
194
+ - time range defaults
195
+ - command parsing for create/update/delete
196
+ - text/json runtime output
197
+ - env config parsing
198
+ - ICS generation and ICS parsing
199
+ - runtime error rendering
200
+
201
+ ## Architecture
202
+
203
+ `icalendar` follows the same layered structure as `threads-cli`:
204
+
205
+ ```text
206
+ cli -> app(commands/use-cases) -> domain -> infra -> presentation -> shared
207
+ ```
208
+
209
+ The CalDAV SDK stays in `infra/`, use-cases orchestrate, domain stays provider-agnostic, and renderers only format output.
210
+
211
+ ## Production-readiness notes
212
+
213
+ - invite flows were smoke-tested live against iCloud CalDAV
214
+ - `--help` and `--version` do not require env credentials
215
+ - organizer display name is configurable with `CALDAV_ORGANIZER_NAME`
216
+ - attendee invites work in both create and update flows
217
+ - CLI defaults to the first calendar when `CALDAV_CALENDAR_NAME` is omitted
218
+
219
+ ## Repo docs
220
+
221
+ - [Agent integration guide](./docs/agent-integration.md)
222
+ - [Architecture](./docs/architecture.md)
223
+ - [Release checklist](./docs/release-checklist.md)
224
+ - [Versioning policy](./docs/versioning.md)
225
+ - [Changelog](./CHANGELOG.md)
226
+ - GitHub Actions: `.github/workflows/ci.yml`, `.github/workflows/release.yml`, `.github/workflows/smoke-icloud.yml`
227
+
228
+ ## License
229
+
230
+ MIT
231
+
232
+ ## Live smoke workflow
233
+
234
+ A dedicated GitHub Actions workflow is included for optional live CalDAV validation through repository secrets.
235
+
236
+ Required secrets:
237
+ - `CALDAV_SERVER_URL`
238
+ - `CALDAV_USERNAME`
239
+ - `CALDAV_PASSWORD`
240
+
241
+ Optional secrets:
242
+ - `CALDAV_CALENDAR_NAME`
243
+ - `CALDAV_ORGANIZER_NAME`
244
+
245
+ The workflow creates a temporary event, updates it, and deletes it in the end.
Binary file
@@ -0,0 +1,500 @@
1
+ // src/cli.ts
2
+ import { cac } from "cac";
3
+
4
+ // src/infra/config/env-config.reader.ts
5
+ import "dotenv/config";
6
+ import { z } from "zod";
7
+ var envSchema = z.object({
8
+ CALDAV_SERVER_URL: z.string().url(),
9
+ CALDAV_USERNAME: z.string().min(1),
10
+ CALDAV_PASSWORD: z.string().min(1),
11
+ CALDAV_CALENDAR_NAME: z.string().min(1).optional(),
12
+ CALDAV_RANGE_START: z.string().datetime({ offset: true }).optional(),
13
+ CALDAV_RANGE_END: z.string().datetime({ offset: true }).optional(),
14
+ CALDAV_EXPAND_RECURRING: z.enum(["true", "false"]).optional().transform((value) => value === "true"),
15
+ CALDAV_ORGANIZER_NAME: z.string().min(1).optional()
16
+ });
17
+ var EnvConfigReader = class {
18
+ readRuntimeConfig() {
19
+ const env = envSchema.parse(process.env);
20
+ return {
21
+ serverUrl: env.CALDAV_SERVER_URL,
22
+ username: env.CALDAV_USERNAME,
23
+ password: env.CALDAV_PASSWORD,
24
+ calendarName: env.CALDAV_CALENDAR_NAME,
25
+ rangeStart: env.CALDAV_RANGE_START,
26
+ rangeEnd: env.CALDAV_RANGE_END,
27
+ expandRecurring: env.CALDAV_EXPAND_RECURRING,
28
+ organizerName: env.CALDAV_ORGANIZER_NAME
29
+ };
30
+ }
31
+ };
32
+
33
+ // src/infra/parsing/ics-parser.ts
34
+ var unfoldIcsLines = (ics) => {
35
+ return ics.replace(/\r\n[ \t]/g, "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
36
+ };
37
+ var readIcsField = (ics, fieldName) => {
38
+ const lines = unfoldIcsLines(ics);
39
+ const line = lines.find((entry) => entry.startsWith(`${fieldName}:`) || entry.startsWith(`${fieldName};`));
40
+ if (!line) return void 0;
41
+ const separatorIndex = line.indexOf(":");
42
+ return separatorIndex === -1 ? void 0 : line.slice(separatorIndex + 1).trim();
43
+ };
44
+ var parseCalendarEvent = (raw, url) => {
45
+ const ics = typeof raw === "string" ? raw : "";
46
+ return {
47
+ summary: readIcsField(ics, "SUMMARY") ?? "Untitled event",
48
+ start: readIcsField(ics, "DTSTART"),
49
+ end: readIcsField(ics, "DTEND"),
50
+ description: readIcsField(ics, "DESCRIPTION"),
51
+ location: readIcsField(ics, "LOCATION"),
52
+ url
53
+ };
54
+ };
55
+
56
+ // src/infra/caldav/calendar-mapper.ts
57
+ var toCalendar = (calendar) => ({
58
+ id: calendar.url,
59
+ displayName: String(calendar.displayName ?? "Untitled"),
60
+ url: calendar.url
61
+ });
62
+ var calendarDisplayName = (calendar) => calendar.displayName || "Untitled";
63
+
64
+ // src/infra/caldav/client.ts
65
+ import tsdav from "tsdav";
66
+ var { createDAVClient } = tsdav;
67
+ var createCaldavClient = async (config) => {
68
+ const client = await createDAVClient({
69
+ serverUrl: config.serverUrl,
70
+ credentials: {
71
+ username: config.username,
72
+ password: config.password
73
+ },
74
+ authMethod: "Basic",
75
+ defaultAccountType: "caldav"
76
+ });
77
+ return client;
78
+ };
79
+
80
+ // src/infra/caldav/event-object.service.ts
81
+ import crypto2 from "crypto";
82
+
83
+ // src/domain/events/ics.ts
84
+ import crypto from "crypto";
85
+
86
+ // src/shared/errors/cli-error.ts
87
+ var CliError = class extends Error {
88
+ code;
89
+ constructor(code, message) {
90
+ super(message);
91
+ this.name = "CliError";
92
+ this.code = code;
93
+ }
94
+ };
95
+
96
+ // src/domain/events/ics.ts
97
+ var escapeText = (value) => value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/,/g, "\\,").replace(/;/g, "\\;");
98
+ var toUtcStamp = (value) => {
99
+ const date = new Date(value);
100
+ if (Number.isNaN(date.getTime())) {
101
+ throw new CliError("invalid_datetime", `Invalid datetime: ${value}`);
102
+ }
103
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
104
+ };
105
+ var renderAttendee = (attendee) => {
106
+ const params = ["CUTYPE=INDIVIDUAL", "ROLE=REQ-PARTICIPANT", "PARTSTAT=NEEDS-ACTION", "RSVP=TRUE"];
107
+ if (attendee.commonName) params.unshift(`CN=${escapeText(attendee.commonName)}`);
108
+ return `ATTENDEE;${params.join(";")}:mailto:${attendee.email}`;
109
+ };
110
+ var renderOrganizer = (organizerEmail, organizerCommonName) => {
111
+ if (organizerCommonName) {
112
+ return `ORGANIZER;CN=${escapeText(organizerCommonName)}:mailto:${organizerEmail}`;
113
+ }
114
+ return `ORGANIZER:mailto:${organizerEmail}`;
115
+ };
116
+ var buildEventIcs = (draft, uid = crypto.randomUUID(), organizerEmail, organizerCommonName) => {
117
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
118
+ const lines = [
119
+ "BEGIN:VCALENDAR",
120
+ "VERSION:2.0",
121
+ "PRODID:-//cayde-6//icalendar//EN",
122
+ draft.attendees?.length ? "METHOD:REQUEST" : "METHOD:PUBLISH",
123
+ "BEGIN:VEVENT",
124
+ `UID:${uid}`,
125
+ `DTSTAMP:${now}`,
126
+ "SEQUENCE:0",
127
+ "STATUS:CONFIRMED",
128
+ `DTSTART:${toUtcStamp(draft.start)}`,
129
+ `DTEND:${toUtcStamp(draft.end)}`,
130
+ `SUMMARY:${escapeText(draft.summary)}`
131
+ ];
132
+ if (organizerEmail) lines.push(renderOrganizer(organizerEmail, organizerCommonName));
133
+ if (draft.description) lines.push(`DESCRIPTION:${escapeText(draft.description)}`);
134
+ if (draft.location) lines.push(`LOCATION:${escapeText(draft.location)}`);
135
+ for (const attendee of draft.attendees || []) lines.push(renderAttendee(attendee));
136
+ lines.push("END:VEVENT", "END:VCALENDAR");
137
+ return lines.join("\r\n") + "\r\n";
138
+ };
139
+
140
+ // src/infra/caldav/event-object.service.ts
141
+ var EventObjectService = class {
142
+ constructor(client, options) {
143
+ this.client = client;
144
+ this.options = options;
145
+ }
146
+ client;
147
+ options;
148
+ async create(calendar, draft) {
149
+ const filename = `${crypto2.randomUUID()}.ics`;
150
+ const objectUrl = new URL(filename, calendar.url.endsWith("/") ? calendar.url : `${calendar.url}/`).toString();
151
+ const iCalString = buildEventIcs(draft, void 0, this.options.organizerEmail, this.options.organizerCommonName);
152
+ await this.client.createCalendarObject({
153
+ calendar: { url: calendar.url },
154
+ filename,
155
+ iCalString
156
+ });
157
+ return objectUrl;
158
+ }
159
+ async update(update) {
160
+ const iCalString = buildEventIcs(update, void 0, this.options.organizerEmail, this.options.organizerCommonName);
161
+ await this.client.updateCalendarObject({
162
+ calendarObject: { url: update.url, data: iCalString }
163
+ });
164
+ }
165
+ async delete(url) {
166
+ await this.client.deleteCalendarObject({ calendarObject: { url } });
167
+ }
168
+ };
169
+
170
+ // src/infra/caldav/tsdav-calendar.gateway.ts
171
+ var TsdavCalendarGateway = class {
172
+ constructor(config) {
173
+ this.config = config;
174
+ this.clientPromise = createCaldavClient(config);
175
+ }
176
+ config;
177
+ clientPromise;
178
+ async listCalendars() {
179
+ const client = await this.clientPromise;
180
+ const calendars = await client.fetchCalendars();
181
+ return calendars.map(toCalendar);
182
+ }
183
+ async listEvents(input) {
184
+ const client = await this.clientPromise;
185
+ const calendars = await client.fetchCalendars();
186
+ const calendar = calendars.find((entry) => entry.url === input.calendar.url);
187
+ if (!calendar) return [];
188
+ const objects = await client.fetchCalendarObjects({
189
+ calendar,
190
+ timeRange: input.timeRange,
191
+ expand: input.expandRecurring
192
+ });
193
+ return objects.map((object) => parseCalendarEvent(object.data, object.url));
194
+ }
195
+ async createEvent(input) {
196
+ const client = await this.clientPromise;
197
+ const service = new EventObjectService(client, {
198
+ organizerEmail: this.config.username,
199
+ organizerCommonName: this.config.organizerName
200
+ });
201
+ const url = await service.create(input.calendar, input.draft);
202
+ return { ok: true, calendarName: calendarDisplayName(input.calendar), url };
203
+ }
204
+ async updateEvent(input) {
205
+ const client = await this.clientPromise;
206
+ const service = new EventObjectService(client, {
207
+ organizerEmail: this.config.username,
208
+ organizerCommonName: this.config.organizerName
209
+ });
210
+ await service.update(input.update);
211
+ return { ok: true, calendarName: calendarDisplayName(input.calendar), url: input.update.url };
212
+ }
213
+ async deleteEvent(input) {
214
+ const client = await this.clientPromise;
215
+ const service = new EventObjectService(client, {
216
+ organizerEmail: this.config.username,
217
+ organizerCommonName: this.config.organizerName
218
+ });
219
+ await service.delete(input.url);
220
+ return { ok: true, calendarName: calendarDisplayName(input.calendar), url: input.url };
221
+ }
222
+ };
223
+
224
+ // src/app/use-cases/calendars/list-calendars.ts
225
+ var listCalendars = async (gateway) => {
226
+ const calendars = await gateway.listCalendars();
227
+ return { ok: true, calendars, count: calendars.length };
228
+ };
229
+
230
+ // src/app/commands/calendars/list.command.ts
231
+ var runCalendarsListCommand = async (gateway) => {
232
+ return listCalendars(gateway);
233
+ };
234
+
235
+ // src/domain/shared/time-range.ts
236
+ var toCalDavTime = (date) => {
237
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
238
+ };
239
+ var addDays = (date, days) => {
240
+ const next = new Date(date);
241
+ next.setUTCDate(next.getUTCDate() + days);
242
+ return next;
243
+ };
244
+ var buildTimeRange = (start, end) => {
245
+ if (!start && !end) return void 0;
246
+ const startDate = start ? new Date(start) : /* @__PURE__ */ new Date();
247
+ const endDate = end ? new Date(end) : addDays(startDate, 30);
248
+ if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
249
+ throw new CliError("invalid_time_range", "Invalid CALDAV_RANGE_START/CALDAV_RANGE_END value");
250
+ }
251
+ if (startDate >= endDate) {
252
+ throw new CliError("invalid_time_range", "CALDAV_RANGE_START must be earlier than CALDAV_RANGE_END");
253
+ }
254
+ return {
255
+ start: toCalDavTime(startDate),
256
+ end: toCalDavTime(endDate)
257
+ };
258
+ };
259
+
260
+ // src/domain/calendars/calendar.ts
261
+ var pickCalendar = (calendars, calendarName) => {
262
+ if (calendars.length === 0) return void 0;
263
+ if (!calendarName) return calendars[0];
264
+ const normalizedTarget = calendarName.trim().toLowerCase();
265
+ return calendars.find((calendar) => calendar.displayName.trim().toLowerCase() == normalizedTarget);
266
+ };
267
+
268
+ // src/app/use-cases/events/helpers.ts
269
+ var resolveCalendar = async (calendars, calendarName) => {
270
+ const selectedCalendar = pickCalendar(calendars, calendarName);
271
+ if (!selectedCalendar) {
272
+ if (calendars.length === 0) {
273
+ throw new CliError("calendar_missing", "No calendars found");
274
+ }
275
+ throw new CliError("calendar_not_found", `Calendar not found: ${calendarName}`);
276
+ }
277
+ return selectedCalendar;
278
+ };
279
+
280
+ // src/app/use-cases/events/list-events.ts
281
+ var listEvents = async (gateway, config) => {
282
+ const calendars = await gateway.listCalendars();
283
+ const selectedCalendar = await resolveCalendar(calendars, config.calendarName);
284
+ const timeRange = buildTimeRange(config.rangeStart, config.rangeEnd);
285
+ const events = await gateway.listEvents({
286
+ calendar: selectedCalendar,
287
+ timeRange,
288
+ expandRecurring: config.expandRecurring
289
+ });
290
+ return { ok: true, calendars, selectedCalendar, events, count: events.length, timeRange };
291
+ };
292
+
293
+ // src/app/commands/events/list.command.ts
294
+ var runEventsListCommand = async (gateway, config) => {
295
+ return listEvents(gateway, config);
296
+ };
297
+
298
+ // src/app/use-cases/events/create-event.ts
299
+ var createEvent = async (gateway, config, draft) => {
300
+ const calendars = await gateway.listCalendars();
301
+ const calendar = await resolveCalendar(calendars, config.calendarName);
302
+ return gateway.createEvent({ calendar, draft });
303
+ };
304
+
305
+ // src/app/commands/events/create.command.ts
306
+ var getFlagValue = (args, name) => {
307
+ const index = args.indexOf(name);
308
+ if (index === -1) return void 0;
309
+ return args[index + 1]?.startsWith("--") ? void 0 : args[index + 1];
310
+ };
311
+ var parseAttendees = (value) => {
312
+ if (!value) return void 0;
313
+ const attendees = value.split(",").map((entry) => entry.trim()).filter(Boolean);
314
+ return attendees.length ? attendees.map((email) => ({ email })) : void 0;
315
+ };
316
+ var runEventsCreateCommand = async (gateway, config, args) => {
317
+ const summary = getFlagValue(args, "--summary");
318
+ const start = getFlagValue(args, "--start");
319
+ const end = getFlagValue(args, "--end");
320
+ const description = getFlagValue(args, "--description");
321
+ const location = getFlagValue(args, "--location");
322
+ const attendees = parseAttendees(getFlagValue(args, "--attendees"));
323
+ if (!summary || !start || !end) {
324
+ throw new CliError("missing_flags", "events create requires --summary, --start, and --end");
325
+ }
326
+ return createEvent(gateway, config, { summary, start, end, description, location, attendees });
327
+ };
328
+
329
+ // src/app/use-cases/events/update-event.ts
330
+ var updateEvent = async (gateway, config, update) => {
331
+ const calendars = await gateway.listCalendars();
332
+ const calendar = await resolveCalendar(calendars, config.calendarName);
333
+ return gateway.updateEvent({ calendar, update });
334
+ };
335
+
336
+ // src/app/commands/events/update.command.ts
337
+ var getFlagValue2 = (args, name) => {
338
+ const index = args.indexOf(name);
339
+ if (index === -1) return void 0;
340
+ return args[index + 1]?.startsWith("--") ? void 0 : args[index + 1];
341
+ };
342
+ var parseAttendees2 = (value) => {
343
+ if (!value) return void 0;
344
+ const attendees = value.split(",").map((entry) => entry.trim()).filter(Boolean);
345
+ return attendees.length ? attendees.map((email) => ({ email })) : void 0;
346
+ };
347
+ var runEventsUpdateCommand = async (gateway, config, args) => {
348
+ const url = getFlagValue2(args, "--url");
349
+ const summary = getFlagValue2(args, "--summary");
350
+ const start = getFlagValue2(args, "--start");
351
+ const end = getFlagValue2(args, "--end");
352
+ const description = getFlagValue2(args, "--description");
353
+ const location = getFlagValue2(args, "--location");
354
+ const attendees = parseAttendees2(getFlagValue2(args, "--attendees"));
355
+ if (!url || !summary || !start || !end) {
356
+ throw new CliError("missing_flags", "events update requires --url, --summary, --start, and --end");
357
+ }
358
+ return updateEvent(gateway, config, { url, summary, start, end, description, location, attendees });
359
+ };
360
+
361
+ // src/app/use-cases/events/delete-event.ts
362
+ var deleteEvent = async (gateway, config, url) => {
363
+ const calendars = await gateway.listCalendars();
364
+ const calendar = await resolveCalendar(calendars, config.calendarName);
365
+ return gateway.deleteEvent({ calendar, url });
366
+ };
367
+
368
+ // src/app/commands/events/delete.command.ts
369
+ var getFlagValue3 = (args, name) => {
370
+ const index = args.indexOf(name);
371
+ if (index === -1) return void 0;
372
+ return args[index + 1]?.startsWith("--") ? void 0 : args[index + 1];
373
+ };
374
+ var runEventsDeleteCommand = async (gateway, config, args) => {
375
+ const url = getFlagValue3(args, "--url") || args[2];
376
+ if (!url) {
377
+ throw new CliError("missing_flags", "events delete requires --url <event-url> or events delete <event-url>");
378
+ }
379
+ return deleteEvent(gateway, config, url);
380
+ };
381
+
382
+ // src/presentation/text/renderers.ts
383
+ var renderCalendarsText = (result) => {
384
+ if (result.calendars.length === 0) return "calendars list: 0 item(s)";
385
+ return [
386
+ `calendars list: ${result.count} item(s)`,
387
+ ...result.calendars.map((calendar) => `- ${calendar.displayName} (${calendar.url})`)
388
+ ].join("\n");
389
+ };
390
+ var renderEventsText = (result) => {
391
+ const lines = [];
392
+ lines.push(`events list: ${result.count} item(s)`);
393
+ if (result.selectedCalendar) {
394
+ const rangeLabel = result.timeRange ? ` | range ${result.timeRange.start} \u2192 ${result.timeRange.end}` : "";
395
+ lines.push(`calendar: ${result.selectedCalendar.displayName}${rangeLabel}`);
396
+ }
397
+ if (result.events.length === 0) {
398
+ lines.push("- No events found.");
399
+ return lines.join("\n");
400
+ }
401
+ for (const event of result.events) {
402
+ lines.push(`- summary: ${event.summary}`);
403
+ lines.push(` start: ${event.start ?? "-"}`);
404
+ lines.push(` end: ${event.end ?? "-"}`);
405
+ lines.push(` location: ${event.location ?? "-"}`);
406
+ lines.push(` url: ${event.url}`);
407
+ }
408
+ return lines.join("\n");
409
+ };
410
+ var renderEventMutationText = (action, result) => {
411
+ return [
412
+ `events ${action}: done`,
413
+ `calendar: ${result.calendarName}`,
414
+ `url: ${result.url}`
415
+ ].join("\n");
416
+ };
417
+
418
+ // src/presentation/json/renderers.ts
419
+ var renderJson = (value) => JSON.stringify(value, null, 2);
420
+
421
+ // src/app/commands/runtime.ts
422
+ var hasFlag = (args, name) => args.includes(name);
423
+ var print = (args, value, renderText) => {
424
+ if (hasFlag(args, "--json")) {
425
+ console.log(renderJson(value));
426
+ return;
427
+ }
428
+ console.log(renderText(value));
429
+ };
430
+ var printError = (args, error) => {
431
+ if (hasFlag(args, "--json")) {
432
+ const normalized = error instanceof CliError ? { ok: false, error: { code: error.code, message: error.message } } : { ok: false, error: { message: error.message } };
433
+ console.error(renderJson(normalized));
434
+ return;
435
+ }
436
+ console.error(error.message);
437
+ };
438
+ var runCommand = async ({ gateway, config, args }) => {
439
+ if (args.length === 0) {
440
+ const result = await runEventsListCommand(gateway, config);
441
+ print(args, result, renderEventsText);
442
+ return true;
443
+ }
444
+ if (args[0] === "calendars" && args[1] === "list") {
445
+ const result = await runCalendarsListCommand(gateway);
446
+ print(args, result, renderCalendarsText);
447
+ return true;
448
+ }
449
+ if (args[0] === "events" && args[1] === "list") {
450
+ const result = await runEventsListCommand(gateway, config);
451
+ print(args, result, renderEventsText);
452
+ return true;
453
+ }
454
+ if (args[0] === "events" && args[1] === "create") {
455
+ const result = await runEventsCreateCommand(gateway, config, args);
456
+ print(args, result, (value) => renderEventMutationText("create", value));
457
+ return true;
458
+ }
459
+ if (args[0] === "events" && args[1] === "update") {
460
+ const result = await runEventsUpdateCommand(gateway, config, args);
461
+ print(args, result, (value) => renderEventMutationText("update", value));
462
+ return true;
463
+ }
464
+ if (args[0] === "events" && args[1] === "delete") {
465
+ const result = await runEventsDeleteCommand(gateway, config, args);
466
+ print(args, result, (value) => renderEventMutationText("delete", value));
467
+ return true;
468
+ }
469
+ return false;
470
+ };
471
+
472
+ // src/cli.ts
473
+ var cli = cac("icalendar");
474
+ cli.command("calendars list", "list available calendars");
475
+ cli.command("events list", "list events for selected calendar/time range");
476
+ cli.command("events create", "create calendar event");
477
+ cli.command("events update", "update calendar event");
478
+ cli.command("events delete [url]", "delete calendar event");
479
+ cli.help();
480
+ cli.version("0.1.0");
481
+ var runCli = async (args = process.argv.slice(2)) => {
482
+ if (args.includes("--help") || args.includes("-h") || args.includes("--version") || args.includes("-v")) {
483
+ cli.parse();
484
+ return;
485
+ }
486
+ const configReader = new EnvConfigReader();
487
+ const config = configReader.readRuntimeConfig();
488
+ const gateway = new TsdavCalendarGateway(config);
489
+ if (!await runCommand({ gateway, config, args })) {
490
+ cli.parse();
491
+ }
492
+ };
493
+ runCli().catch((error) => {
494
+ printError(process.argv.slice(2), error);
495
+ process.exitCode = 1;
496
+ });
497
+
498
+ export {
499
+ runCli
500
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare const runCli: (args?: string[]) => Promise<void>;
2
+
3
+ export { runCli };
package/dist/cli.js ADDED
@@ -0,0 +1,6 @@
1
+ import {
2
+ runCli
3
+ } from "./chunk-MSLA47CB.js";
4
+ export {
5
+ runCli
6
+ };
@@ -0,0 +1 @@
1
+ export { runCli } from './cli.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import {
2
+ runCli
3
+ } from "./chunk-MSLA47CB.js";
4
+ export {
5
+ runCli
6
+ };
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@cayde-6/icalendar",
3
+ "version": "0.1.0",
4
+ "description": "Production-ready TypeScript CLI for CalDAV calendars, iCalendar events, and agent automation",
5
+ "type": "module",
6
+ "private": false,
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "bin": {
10
+ "icalendar": "./dist/cli.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE",
22
+ "assets"
23
+ ],
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "scripts": {
28
+ "test": "bash -lc 'node --import tsx --test $(find test -name \"*.test.ts\" -print)'",
29
+ "test:coverage": "bash -lc 'c8 --reporter=text --reporter=lcov node --import tsx --test $(find test -name \"*.test.ts\" -print)'",
30
+ "build": "tsup src/cli.ts src/index.ts --format esm --dts --clean",
31
+ "dev": "tsx src/cli.ts",
32
+ "check": "tsc --noEmit -p tsconfig.json",
33
+ "verify": "npm run check && npm run build && npm run test:coverage",
34
+ "start": "node dist/cli.js"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/cayde-6/icalendar.git"
39
+ },
40
+ "keywords": [
41
+ "caldav",
42
+ "icalendar",
43
+ "ics",
44
+ "cli",
45
+ "automation",
46
+ "agents",
47
+ "icloud"
48
+ ],
49
+ "author": "cayde-6",
50
+ "license": "MIT",
51
+ "bugs": {
52
+ "url": "https://github.com/cayde-6/icalendar/issues"
53
+ },
54
+ "homepage": "https://github.com/cayde-6/icalendar#readme",
55
+ "dependencies": {
56
+ "cac": "^6.7.14",
57
+ "dotenv": "^17.4.2",
58
+ "tsdav": "^2.2.0",
59
+ "zod": "^4.4.1"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^25.6.0",
63
+ "c8": "^11.0.0",
64
+ "tsup": "^8.5.0",
65
+ "tsx": "^4.21.0",
66
+ "typescript": "^6.0.3"
67
+ },
68
+ "publishConfig": {
69
+ "access": "public"
70
+ }
71
+ }