@hasna/calendar 0.1.7 → 0.1.9

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/dist/cli/index.js CHANGED
@@ -3241,6 +3241,82 @@ function deleteCalendar(id, db) {
3241
3241
  return result.changes > 0;
3242
3242
  }
3243
3243
 
3244
+ // src/db/event-time.ts
3245
+ var ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/;
3246
+ var NS_PER_MS = 1000000n;
3247
+ var NS_PER_MINUTE = 60000000000n;
3248
+ function daysInMonth(year, month) {
3249
+ const date = new Date(0);
3250
+ date.setUTCFullYear(year, month, 0);
3251
+ date.setUTCHours(0, 0, 0, 0);
3252
+ return date.getUTCDate();
3253
+ }
3254
+ function timestampBaseMs(year, month, day, hour, minute, second) {
3255
+ const date = new Date(0);
3256
+ date.setUTCFullYear(year, month - 1, day);
3257
+ date.setUTCHours(hour, minute, second, 0);
3258
+ return date.getTime();
3259
+ }
3260
+ function parseEventTimestamp(value) {
3261
+ const match = ISO_DATE_TIME_RE.exec(value);
3262
+ if (!match) {
3263
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
3264
+ }
3265
+ const year = Number(match[1]);
3266
+ const month = Number(match[2]);
3267
+ const day = Number(match[3]);
3268
+ const hour = Number(match[4]);
3269
+ const minute = Number(match[5]);
3270
+ const second = Number(match[6]);
3271
+ const fraction = match[7] || "";
3272
+ const offset = match[8];
3273
+ const maxDay = month >= 1 && month <= 12 ? daysInMonth(year, month) : 0;
3274
+ if (month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59) {
3275
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
3276
+ }
3277
+ let offsetMinutes = 0;
3278
+ if (offset !== "Z") {
3279
+ const offsetSign = offset[0] === "-" ? -1 : 1;
3280
+ const offsetHour = Number(offset.slice(1, 3));
3281
+ const offsetMinute = Number(offset.slice(4, 6));
3282
+ if (offsetHour > 23 || offsetMinute > 59) {
3283
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
3284
+ }
3285
+ offsetMinutes = offsetSign * (offsetHour * 60 + offsetMinute);
3286
+ }
3287
+ const timestamp = timestampBaseMs(year, month, day, hour, minute, second);
3288
+ if (!Number.isFinite(timestamp)) {
3289
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
3290
+ }
3291
+ const fractionalNs = BigInt(fraction.padEnd(9, "0") || "0");
3292
+ return BigInt(timestamp) * NS_PER_MS + fractionalNs - BigInt(offsetMinutes) * NS_PER_MINUTE;
3293
+ }
3294
+ function assertEventEndsAfterStart(startAt, endAt) {
3295
+ const start = parseEventTimestamp(startAt);
3296
+ const end = parseEventTimestamp(endAt);
3297
+ if (end <= start) {
3298
+ throw new RangeError("Event end_at must be after start_at");
3299
+ }
3300
+ }
3301
+ function parseTimeRange(startAt, endAt) {
3302
+ const start = parseEventTimestamp(startAt);
3303
+ const end = parseEventTimestamp(endAt);
3304
+ if (end <= start) {
3305
+ throw new RangeError("Time range end must be after start");
3306
+ }
3307
+ return { start, end };
3308
+ }
3309
+ function compareEventInstants(a, b) {
3310
+ if (a < b)
3311
+ return -1;
3312
+ if (a > b)
3313
+ return 1;
3314
+ return 0;
3315
+ }
3316
+ function compareEventTimestampStrings(a, b) {
3317
+ return compareEventInstants(parseEventTimestamp(a), parseEventTimestamp(b)) || a.localeCompare(b);
3318
+ }
3319
+
3244
3320
  // src/db/events.ts
3245
3321
  function rowToEvent(row) {
3246
3322
  return {
@@ -3266,9 +3342,13 @@ function rowToEvent(row) {
3266
3342
  updated_at: row.updated_at
3267
3343
  };
3268
3344
  }
3345
+ function positiveInteger(value) {
3346
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
3347
+ }
3269
3348
  function createEvent2(input, db) {
3270
3349
  db = db || getDatabase();
3271
3350
  const id = crypto.randomUUID().slice(0, 8);
3351
+ assertEventEndsAfterStart(input.start_at, input.end_at);
3272
3352
  db.run(`INSERT INTO events (id, calendar_id, org_id, title, description, location, start_at, end_at, all_day, timezone, status, busy_type, visibility, recurrence_rule, recurrence_exception_dates, source_task_id, created_by, metadata)
3273
3353
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.calendar_id, input.org_id, input.title, input.description || null, input.location || null, input.start_at, input.end_at, input.all_day ? 1 : 0, input.timezone || "UTC", input.status || "confirmed", input.busy_type || "busy", input.visibility || "default", input.recurrence_rule || null, input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null, input.source_task_id || null, input.created_by || null, JSON.stringify(input.metadata || {})]);
3274
3354
  return getEvent(id, db);
@@ -3282,6 +3362,8 @@ function listEvents(filter = {}, db) {
3282
3362
  db = db || getDatabase();
3283
3363
  const conditions = [];
3284
3364
  const params = [];
3365
+ const after = filter.after ? parseEventTimestamp(filter.after) : null;
3366
+ const before = filter.before ? parseEventTimestamp(filter.before) : null;
3285
3367
  if (filter.calendar_id) {
3286
3368
  conditions.push("calendar_id = ?");
3287
3369
  params.push(filter.calendar_id);
@@ -3294,14 +3376,6 @@ function listEvents(filter = {}, db) {
3294
3376
  conditions.push("status = ?");
3295
3377
  params.push(filter.status);
3296
3378
  }
3297
- if (filter.after) {
3298
- conditions.push("start_at >= ?");
3299
- params.push(filter.after);
3300
- }
3301
- if (filter.before) {
3302
- conditions.push("start_at <= ?");
3303
- params.push(filter.before);
3304
- }
3305
3379
  if (filter.created_by) {
3306
3380
  conditions.push("created_by = ?");
3307
3381
  params.push(filter.created_by);
@@ -3311,16 +3385,20 @@ function listEvents(filter = {}, db) {
3311
3385
  params.push(filter.source_task_id);
3312
3386
  }
3313
3387
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3314
- const limit = filter.limit ? `LIMIT ${filter.limit}` : "";
3315
- const offset = filter.offset ? `OFFSET ${filter.offset}` : "";
3316
- const rows = db.query(`SELECT * FROM events ${where} ORDER BY start_at ${limit} ${offset}`).all(...params);
3317
- return rows.map(rowToEvent);
3388
+ const rows = db.query(`SELECT * FROM events ${where}`).all(...params);
3389
+ const events = rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at) })).filter(({ start }) => (after === null || start >= after) && (before === null || start <= before)).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
3390
+ const offset = positiveInteger(filter.offset) || 0;
3391
+ const limit = positiveInteger(filter.limit);
3392
+ return limit ? events.slice(offset, offset + limit) : events.slice(offset);
3318
3393
  }
3319
3394
  function updateEvent(id, input, db) {
3320
3395
  db = db || getDatabase();
3321
3396
  const existing = getEvent(id, db);
3322
3397
  if (!existing)
3323
3398
  throw new NotFoundError("Event", id);
3399
+ const startAt = input.start_at ?? existing.start_at;
3400
+ const endAt = input.end_at ?? existing.end_at;
3401
+ assertEventEndsAfterStart(startAt, endAt);
3324
3402
  db.run(`UPDATE events SET title = ?, description = ?, location = ?, start_at = ?, end_at = ?, all_day = ?, timezone = ?, status = ?, busy_type = ?, visibility = ?, recurrence_rule = ?, recurrence_exception_dates = ?, source_task_id = ?, metadata = ?, updated_at = datetime('now') WHERE id = ?`, [input.title ?? existing.title, input.description !== undefined ? input.description : existing.description, input.location !== undefined ? input.location : existing.location, input.start_at ?? existing.start_at, input.end_at ?? existing.end_at, input.all_day !== undefined ? input.all_day ? 1 : 0 : existing.all_day ? 1 : 0, input.timezone ?? existing.timezone, input.status ?? existing.status, input.busy_type ?? existing.busy_type, input.visibility ?? existing.visibility, input.recurrence_rule !== undefined ? input.recurrence_rule : existing.recurrence_rule, input.recurrence_exception_dates !== undefined ? input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null : existing.recurrence_exception_dates ? JSON.stringify(existing.recurrence_exception_dates) : null, input.source_task_id !== undefined ? input.source_task_id : existing.source_task_id, JSON.stringify(input.metadata ?? existing.metadata), id]);
3325
3403
  return getEvent(id, db);
3326
3404
  }
@@ -3332,18 +3410,18 @@ function deleteEvent(id, db) {
3332
3410
  function findConflicts(calendarId, range, excludeEventId, db) {
3333
3411
  db = db || getDatabase();
3334
3412
  const exclude = excludeEventId ? "AND id != ?" : "";
3335
- const params = excludeEventId ? [calendarId, range.end, range.start, excludeEventId] : [calendarId, range.end, range.start];
3336
- const rows = db.query(`SELECT * FROM events WHERE calendar_id = ? AND start_at < ? AND end_at > ? AND status != 'cancelled' ${exclude} ORDER BY start_at`).all(...params);
3337
- return rows.map(rowToEvent);
3413
+ const params = excludeEventId ? [calendarId, excludeEventId] : [calendarId];
3414
+ const { start: rangeStart, end: rangeEnd } = parseTimeRange(range.start, range.end);
3415
+ const rows = db.query(`SELECT * FROM events WHERE calendar_id = ? AND status != 'cancelled' ${exclude}`).all(...params);
3416
+ return rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at), end: parseEventTimestamp(event.end_at) })).filter(({ start, end }) => start < rangeEnd && end > rangeStart).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
3338
3417
  }
3339
3418
  function searchEvents(query, orgId, db) {
3340
3419
  db = db || getDatabase();
3341
3420
  const rows = db.query(`SELECT e.* FROM events e
3342
3421
  INNER JOIN events_fts f ON f.rowid = e.rowid
3343
3422
  WHERE events_fts MATCH ?
3344
- ${orgId ? "AND e.org_id = ?" : ""}
3345
- ORDER BY e.start_at`).all(query, ...orgId ? [orgId] : []);
3346
- return rows.map(rowToEvent);
3423
+ ${orgId ? "AND e.org_id = ?" : ""}`).all(query, ...orgId ? [orgId] : []);
3424
+ return rows.map(rowToEvent).sort((a, b) => compareEventTimestampStrings(a.start_at, b.start_at));
3347
3425
  }
3348
3426
 
3349
3427
  // src/db/attendees.ts
@@ -1 +1 @@
1
- {"version":3,"file":"attendees.d.ts","sourceRoot":"","sources":["../../src/db/attendees.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAkBjG,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,CAUvF;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,GAAG,IAAI,CAI3E;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,EAAE,CAIpF;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,EAAE,CASjF;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,CAYnG;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAIjE;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAIhF"}
1
+ {"version":3,"file":"attendees.d.ts","sourceRoot":"","sources":["../../src/db/attendees.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAkBjG,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,CAUvF;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,GAAG,IAAI,CAI3E;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,EAAE,CAIpF;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,EAAE,CAUjF;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,CAYnG;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAIjE;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAIhF"}
@@ -0,0 +1,9 @@
1
+ export declare function parseEventTimestamp(value: string): bigint;
2
+ export declare function assertEventEndsAfterStart(startAt: string, endAt: string): void;
3
+ export declare function parseTimeRange(startAt: string, endAt: string): {
4
+ start: bigint;
5
+ end: bigint;
6
+ };
7
+ export declare function compareEventInstants(a: bigint, b: bigint): number;
8
+ export declare function compareEventTimestampStrings(a: string, b: string): number;
9
+ //# sourceMappingURL=event-time.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-time.d.ts","sourceRoot":"","sources":["../../src/db/event-time.ts"],"names":[],"mappings":"AAkBA,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAsCzD;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAO9E;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAS7F;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAIjE;AAED,wBAAgB,4BAA4B,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzE"}
@@ -1 +1 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/db/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AA4BnF,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,CAWzE;AAED,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,IAAI,CAIhE;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,UAAU,CAAC,MAAM,GAAE,gBAAqB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAmBhF;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,CAWrF;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAI9D;AAID,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,uEAAuE;AACvE,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAWnH;AAED,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAarH;AAID,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAWlF"}
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/db/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,OAAO,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAgCnF,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,CAYzE;AAED,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,IAAI,CAIhE;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,UAAU,CAAC,MAAM,GAAE,gBAAqB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAyBhF;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,CAcrF;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAI9D;AAID,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,uEAAuE;AACvE,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAiBnH;AAED,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAkBrH;AAID,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAYlF"}
package/dist/index.js CHANGED
@@ -518,6 +518,82 @@ function deleteCalendar(id, db) {
518
518
  const result = db.run(`DELETE FROM calendars WHERE id = ?`, [id]);
519
519
  return result.changes > 0;
520
520
  }
521
+ // src/db/event-time.ts
522
+ var ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/;
523
+ var NS_PER_MS = 1000000n;
524
+ var NS_PER_MINUTE = 60000000000n;
525
+ function daysInMonth(year, month) {
526
+ const date = new Date(0);
527
+ date.setUTCFullYear(year, month, 0);
528
+ date.setUTCHours(0, 0, 0, 0);
529
+ return date.getUTCDate();
530
+ }
531
+ function timestampBaseMs(year, month, day, hour, minute, second) {
532
+ const date = new Date(0);
533
+ date.setUTCFullYear(year, month - 1, day);
534
+ date.setUTCHours(hour, minute, second, 0);
535
+ return date.getTime();
536
+ }
537
+ function parseEventTimestamp(value) {
538
+ const match = ISO_DATE_TIME_RE.exec(value);
539
+ if (!match) {
540
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
541
+ }
542
+ const year = Number(match[1]);
543
+ const month = Number(match[2]);
544
+ const day = Number(match[3]);
545
+ const hour = Number(match[4]);
546
+ const minute = Number(match[5]);
547
+ const second = Number(match[6]);
548
+ const fraction = match[7] || "";
549
+ const offset = match[8];
550
+ const maxDay = month >= 1 && month <= 12 ? daysInMonth(year, month) : 0;
551
+ if (month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59) {
552
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
553
+ }
554
+ let offsetMinutes = 0;
555
+ if (offset !== "Z") {
556
+ const offsetSign = offset[0] === "-" ? -1 : 1;
557
+ const offsetHour = Number(offset.slice(1, 3));
558
+ const offsetMinute = Number(offset.slice(4, 6));
559
+ if (offsetHour > 23 || offsetMinute > 59) {
560
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
561
+ }
562
+ offsetMinutes = offsetSign * (offsetHour * 60 + offsetMinute);
563
+ }
564
+ const timestamp = timestampBaseMs(year, month, day, hour, minute, second);
565
+ if (!Number.isFinite(timestamp)) {
566
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
567
+ }
568
+ const fractionalNs = BigInt(fraction.padEnd(9, "0") || "0");
569
+ return BigInt(timestamp) * NS_PER_MS + fractionalNs - BigInt(offsetMinutes) * NS_PER_MINUTE;
570
+ }
571
+ function assertEventEndsAfterStart(startAt, endAt) {
572
+ const start = parseEventTimestamp(startAt);
573
+ const end = parseEventTimestamp(endAt);
574
+ if (end <= start) {
575
+ throw new RangeError("Event end_at must be after start_at");
576
+ }
577
+ }
578
+ function parseTimeRange(startAt, endAt) {
579
+ const start = parseEventTimestamp(startAt);
580
+ const end = parseEventTimestamp(endAt);
581
+ if (end <= start) {
582
+ throw new RangeError("Time range end must be after start");
583
+ }
584
+ return { start, end };
585
+ }
586
+ function compareEventInstants(a, b) {
587
+ if (a < b)
588
+ return -1;
589
+ if (a > b)
590
+ return 1;
591
+ return 0;
592
+ }
593
+ function compareEventTimestampStrings(a, b) {
594
+ return compareEventInstants(parseEventTimestamp(a), parseEventTimestamp(b)) || a.localeCompare(b);
595
+ }
596
+
521
597
  // src/db/events.ts
522
598
  function rowToEvent(row) {
523
599
  return {
@@ -543,9 +619,13 @@ function rowToEvent(row) {
543
619
  updated_at: row.updated_at
544
620
  };
545
621
  }
622
+ function positiveInteger(value) {
623
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
624
+ }
546
625
  function createEvent(input, db) {
547
626
  db = db || getDatabase();
548
627
  const id = crypto.randomUUID().slice(0, 8);
628
+ assertEventEndsAfterStart(input.start_at, input.end_at);
549
629
  db.run(`INSERT INTO events (id, calendar_id, org_id, title, description, location, start_at, end_at, all_day, timezone, status, busy_type, visibility, recurrence_rule, recurrence_exception_dates, source_task_id, created_by, metadata)
550
630
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.calendar_id, input.org_id, input.title, input.description || null, input.location || null, input.start_at, input.end_at, input.all_day ? 1 : 0, input.timezone || "UTC", input.status || "confirmed", input.busy_type || "busy", input.visibility || "default", input.recurrence_rule || null, input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null, input.source_task_id || null, input.created_by || null, JSON.stringify(input.metadata || {})]);
551
631
  return getEvent(id, db);
@@ -559,6 +639,8 @@ function listEvents(filter = {}, db) {
559
639
  db = db || getDatabase();
560
640
  const conditions = [];
561
641
  const params = [];
642
+ const after = filter.after ? parseEventTimestamp(filter.after) : null;
643
+ const before = filter.before ? parseEventTimestamp(filter.before) : null;
562
644
  if (filter.calendar_id) {
563
645
  conditions.push("calendar_id = ?");
564
646
  params.push(filter.calendar_id);
@@ -571,14 +653,6 @@ function listEvents(filter = {}, db) {
571
653
  conditions.push("status = ?");
572
654
  params.push(filter.status);
573
655
  }
574
- if (filter.after) {
575
- conditions.push("start_at >= ?");
576
- params.push(filter.after);
577
- }
578
- if (filter.before) {
579
- conditions.push("start_at <= ?");
580
- params.push(filter.before);
581
- }
582
656
  if (filter.created_by) {
583
657
  conditions.push("created_by = ?");
584
658
  params.push(filter.created_by);
@@ -588,16 +662,20 @@ function listEvents(filter = {}, db) {
588
662
  params.push(filter.source_task_id);
589
663
  }
590
664
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
591
- const limit = filter.limit ? `LIMIT ${filter.limit}` : "";
592
- const offset = filter.offset ? `OFFSET ${filter.offset}` : "";
593
- const rows = db.query(`SELECT * FROM events ${where} ORDER BY start_at ${limit} ${offset}`).all(...params);
594
- return rows.map(rowToEvent);
665
+ const rows = db.query(`SELECT * FROM events ${where}`).all(...params);
666
+ const events = rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at) })).filter(({ start }) => (after === null || start >= after) && (before === null || start <= before)).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
667
+ const offset = positiveInteger(filter.offset) || 0;
668
+ const limit = positiveInteger(filter.limit);
669
+ return limit ? events.slice(offset, offset + limit) : events.slice(offset);
595
670
  }
596
671
  function updateEvent(id, input, db) {
597
672
  db = db || getDatabase();
598
673
  const existing = getEvent(id, db);
599
674
  if (!existing)
600
675
  throw new NotFoundError("Event", id);
676
+ const startAt = input.start_at ?? existing.start_at;
677
+ const endAt = input.end_at ?? existing.end_at;
678
+ assertEventEndsAfterStart(startAt, endAt);
601
679
  db.run(`UPDATE events SET title = ?, description = ?, location = ?, start_at = ?, end_at = ?, all_day = ?, timezone = ?, status = ?, busy_type = ?, visibility = ?, recurrence_rule = ?, recurrence_exception_dates = ?, source_task_id = ?, metadata = ?, updated_at = datetime('now') WHERE id = ?`, [input.title ?? existing.title, input.description !== undefined ? input.description : existing.description, input.location !== undefined ? input.location : existing.location, input.start_at ?? existing.start_at, input.end_at ?? existing.end_at, input.all_day !== undefined ? input.all_day ? 1 : 0 : existing.all_day ? 1 : 0, input.timezone ?? existing.timezone, input.status ?? existing.status, input.busy_type ?? existing.busy_type, input.visibility ?? existing.visibility, input.recurrence_rule !== undefined ? input.recurrence_rule : existing.recurrence_rule, input.recurrence_exception_dates !== undefined ? input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null : existing.recurrence_exception_dates ? JSON.stringify(existing.recurrence_exception_dates) : null, input.source_task_id !== undefined ? input.source_task_id : existing.source_task_id, JSON.stringify(input.metadata ?? existing.metadata), id]);
602
680
  return getEvent(id, db);
603
681
  }
@@ -609,28 +687,28 @@ function deleteEvent(id, db) {
609
687
  function findConflicts(calendarId, range, excludeEventId, db) {
610
688
  db = db || getDatabase();
611
689
  const exclude = excludeEventId ? "AND id != ?" : "";
612
- const params = excludeEventId ? [calendarId, range.end, range.start, excludeEventId] : [calendarId, range.end, range.start];
613
- const rows = db.query(`SELECT * FROM events WHERE calendar_id = ? AND start_at < ? AND end_at > ? AND status != 'cancelled' ${exclude} ORDER BY start_at`).all(...params);
614
- return rows.map(rowToEvent);
690
+ const params = excludeEventId ? [calendarId, excludeEventId] : [calendarId];
691
+ const { start: rangeStart, end: rangeEnd } = parseTimeRange(range.start, range.end);
692
+ const rows = db.query(`SELECT * FROM events WHERE calendar_id = ? AND status != 'cancelled' ${exclude}`).all(...params);
693
+ return rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at), end: parseEventTimestamp(event.end_at) })).filter(({ start, end }) => start < rangeEnd && end > rangeStart).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
615
694
  }
616
695
  function findAgentConflicts(agentId, range, excludeEventId, db) {
617
696
  db = db || getDatabase();
618
697
  const exclude = excludeEventId ? "AND e.id != ?" : "";
619
- const params = excludeEventId ? [agentId, range.end, range.start, excludeEventId] : [agentId, range.end, range.start];
698
+ const params = excludeEventId ? [agentId, excludeEventId] : [agentId];
699
+ const { start: rangeStart, end: rangeEnd } = parseTimeRange(range.start, range.end);
620
700
  const rows = db.query(`SELECT e.* FROM events e
621
701
  INNER JOIN event_attendees a ON a.event_id = e.id
622
- WHERE a.agent_id = ? AND e.start_at < ? AND e.end_at > ? AND e.status != 'cancelled' ${exclude}
623
- ORDER BY e.start_at`).all(...params);
624
- return rows.map(rowToEvent);
702
+ WHERE a.agent_id = ? AND e.status != 'cancelled' ${exclude}`).all(...params);
703
+ return rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at), end: parseEventTimestamp(event.end_at) })).filter(({ start, end }) => start < rangeEnd && end > rangeStart).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
625
704
  }
626
705
  function searchEvents(query, orgId, db) {
627
706
  db = db || getDatabase();
628
707
  const rows = db.query(`SELECT e.* FROM events e
629
708
  INNER JOIN events_fts f ON f.rowid = e.rowid
630
709
  WHERE events_fts MATCH ?
631
- ${orgId ? "AND e.org_id = ?" : ""}
632
- ORDER BY e.start_at`).all(query, ...orgId ? [orgId] : []);
633
- return rows.map(rowToEvent);
710
+ ${orgId ? "AND e.org_id = ?" : ""}`).all(query, ...orgId ? [orgId] : []);
711
+ return rows.map(rowToEvent).sort((a, b) => compareEventTimestampStrings(a.start_at, b.start_at));
634
712
  }
635
713
  // src/db/attendees.ts
636
714
  function rowToAttendee(row) {
@@ -665,10 +743,10 @@ function getAttendeesForEvent(eventId, db) {
665
743
  }
666
744
  function getEventsForAgent(agentId, db) {
667
745
  db = db || getDatabase();
668
- const rows = db.query(`SELECT a.* FROM event_attendees a
746
+ const rows = db.query(`SELECT a.*, e.start_at AS event_start_at FROM event_attendees a
669
747
  INNER JOIN events e ON e.id = a.event_id
670
- WHERE a.agent_id = ? AND e.status != 'cancelled'
671
- ORDER BY e.start_at`).all(agentId);
748
+ WHERE a.agent_id = ? AND e.status != 'cancelled'`).all(agentId);
749
+ rows.sort((a, b) => compareEventTimestampStrings(a.event_start_at, b.event_start_at));
672
750
  return rows.map(rowToAttendee);
673
751
  }
674
752
  function updateAttendee(id, input, db) {
package/dist/mcp/index.js CHANGED
@@ -4423,6 +4423,83 @@ var init_calendars = __esm(() => {
4423
4423
  init_types2();
4424
4424
  });
4425
4425
 
4426
+ // src/db/event-time.ts
4427
+ function daysInMonth(year, month) {
4428
+ const date = new Date(0);
4429
+ date.setUTCFullYear(year, month, 0);
4430
+ date.setUTCHours(0, 0, 0, 0);
4431
+ return date.getUTCDate();
4432
+ }
4433
+ function timestampBaseMs(year, month, day, hour, minute, second) {
4434
+ const date = new Date(0);
4435
+ date.setUTCFullYear(year, month - 1, day);
4436
+ date.setUTCHours(hour, minute, second, 0);
4437
+ return date.getTime();
4438
+ }
4439
+ function parseEventTimestamp(value) {
4440
+ const match = ISO_DATE_TIME_RE.exec(value);
4441
+ if (!match) {
4442
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
4443
+ }
4444
+ const year = Number(match[1]);
4445
+ const month = Number(match[2]);
4446
+ const day = Number(match[3]);
4447
+ const hour = Number(match[4]);
4448
+ const minute = Number(match[5]);
4449
+ const second = Number(match[6]);
4450
+ const fraction = match[7] || "";
4451
+ const offset = match[8];
4452
+ const maxDay = month >= 1 && month <= 12 ? daysInMonth(year, month) : 0;
4453
+ if (month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59) {
4454
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
4455
+ }
4456
+ let offsetMinutes = 0;
4457
+ if (offset !== "Z") {
4458
+ const offsetSign = offset[0] === "-" ? -1 : 1;
4459
+ const offsetHour = Number(offset.slice(1, 3));
4460
+ const offsetMinute = Number(offset.slice(4, 6));
4461
+ if (offsetHour > 23 || offsetMinute > 59) {
4462
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
4463
+ }
4464
+ offsetMinutes = offsetSign * (offsetHour * 60 + offsetMinute);
4465
+ }
4466
+ const timestamp = timestampBaseMs(year, month, day, hour, minute, second);
4467
+ if (!Number.isFinite(timestamp)) {
4468
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
4469
+ }
4470
+ const fractionalNs = BigInt(fraction.padEnd(9, "0") || "0");
4471
+ return BigInt(timestamp) * NS_PER_MS + fractionalNs - BigInt(offsetMinutes) * NS_PER_MINUTE;
4472
+ }
4473
+ function assertEventEndsAfterStart(startAt, endAt) {
4474
+ const start = parseEventTimestamp(startAt);
4475
+ const end = parseEventTimestamp(endAt);
4476
+ if (end <= start) {
4477
+ throw new RangeError("Event end_at must be after start_at");
4478
+ }
4479
+ }
4480
+ function parseTimeRange(startAt, endAt) {
4481
+ const start = parseEventTimestamp(startAt);
4482
+ const end = parseEventTimestamp(endAt);
4483
+ if (end <= start) {
4484
+ throw new RangeError("Time range end must be after start");
4485
+ }
4486
+ return { start, end };
4487
+ }
4488
+ function compareEventInstants(a, b) {
4489
+ if (a < b)
4490
+ return -1;
4491
+ if (a > b)
4492
+ return 1;
4493
+ return 0;
4494
+ }
4495
+ function compareEventTimestampStrings(a, b) {
4496
+ return compareEventInstants(parseEventTimestamp(a), parseEventTimestamp(b)) || a.localeCompare(b);
4497
+ }
4498
+ var ISO_DATE_TIME_RE, NS_PER_MS = 1000000n, NS_PER_MINUTE = 60000000000n;
4499
+ var init_event_time = __esm(() => {
4500
+ ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/;
4501
+ });
4502
+
4426
4503
  // src/db/events.ts
4427
4504
  function rowToEvent(row) {
4428
4505
  return {
@@ -4448,9 +4525,13 @@ function rowToEvent(row) {
4448
4525
  updated_at: row.updated_at
4449
4526
  };
4450
4527
  }
4528
+ function positiveInteger(value) {
4529
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
4530
+ }
4451
4531
  function createEvent(input, db) {
4452
4532
  db = db || getDatabase();
4453
4533
  const id = crypto.randomUUID().slice(0, 8);
4534
+ assertEventEndsAfterStart(input.start_at, input.end_at);
4454
4535
  db.run(`INSERT INTO events (id, calendar_id, org_id, title, description, location, start_at, end_at, all_day, timezone, status, busy_type, visibility, recurrence_rule, recurrence_exception_dates, source_task_id, created_by, metadata)
4455
4536
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.calendar_id, input.org_id, input.title, input.description || null, input.location || null, input.start_at, input.end_at, input.all_day ? 1 : 0, input.timezone || "UTC", input.status || "confirmed", input.busy_type || "busy", input.visibility || "default", input.recurrence_rule || null, input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null, input.source_task_id || null, input.created_by || null, JSON.stringify(input.metadata || {})]);
4456
4537
  return getEvent(id, db);
@@ -4464,6 +4545,8 @@ function listEvents(filter = {}, db) {
4464
4545
  db = db || getDatabase();
4465
4546
  const conditions = [];
4466
4547
  const params = [];
4548
+ const after = filter.after ? parseEventTimestamp(filter.after) : null;
4549
+ const before = filter.before ? parseEventTimestamp(filter.before) : null;
4467
4550
  if (filter.calendar_id) {
4468
4551
  conditions.push("calendar_id = ?");
4469
4552
  params.push(filter.calendar_id);
@@ -4476,14 +4559,6 @@ function listEvents(filter = {}, db) {
4476
4559
  conditions.push("status = ?");
4477
4560
  params.push(filter.status);
4478
4561
  }
4479
- if (filter.after) {
4480
- conditions.push("start_at >= ?");
4481
- params.push(filter.after);
4482
- }
4483
- if (filter.before) {
4484
- conditions.push("start_at <= ?");
4485
- params.push(filter.before);
4486
- }
4487
4562
  if (filter.created_by) {
4488
4563
  conditions.push("created_by = ?");
4489
4564
  params.push(filter.created_by);
@@ -4493,16 +4568,20 @@ function listEvents(filter = {}, db) {
4493
4568
  params.push(filter.source_task_id);
4494
4569
  }
4495
4570
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4496
- const limit = filter.limit ? `LIMIT ${filter.limit}` : "";
4497
- const offset = filter.offset ? `OFFSET ${filter.offset}` : "";
4498
- const rows = db.query(`SELECT * FROM events ${where} ORDER BY start_at ${limit} ${offset}`).all(...params);
4499
- return rows.map(rowToEvent);
4571
+ const rows = db.query(`SELECT * FROM events ${where}`).all(...params);
4572
+ const events = rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at) })).filter(({ start }) => (after === null || start >= after) && (before === null || start <= before)).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
4573
+ const offset = positiveInteger(filter.offset) || 0;
4574
+ const limit = positiveInteger(filter.limit);
4575
+ return limit ? events.slice(offset, offset + limit) : events.slice(offset);
4500
4576
  }
4501
4577
  function updateEvent(id, input, db) {
4502
4578
  db = db || getDatabase();
4503
4579
  const existing = getEvent(id, db);
4504
4580
  if (!existing)
4505
4581
  throw new NotFoundError("Event", id);
4582
+ const startAt = input.start_at ?? existing.start_at;
4583
+ const endAt = input.end_at ?? existing.end_at;
4584
+ assertEventEndsAfterStart(startAt, endAt);
4506
4585
  db.run(`UPDATE events SET title = ?, description = ?, location = ?, start_at = ?, end_at = ?, all_day = ?, timezone = ?, status = ?, busy_type = ?, visibility = ?, recurrence_rule = ?, recurrence_exception_dates = ?, source_task_id = ?, metadata = ?, updated_at = datetime('now') WHERE id = ?`, [input.title ?? existing.title, input.description !== undefined ? input.description : existing.description, input.location !== undefined ? input.location : existing.location, input.start_at ?? existing.start_at, input.end_at ?? existing.end_at, input.all_day !== undefined ? input.all_day ? 1 : 0 : existing.all_day ? 1 : 0, input.timezone ?? existing.timezone, input.status ?? existing.status, input.busy_type ?? existing.busy_type, input.visibility ?? existing.visibility, input.recurrence_rule !== undefined ? input.recurrence_rule : existing.recurrence_rule, input.recurrence_exception_dates !== undefined ? input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null : existing.recurrence_exception_dates ? JSON.stringify(existing.recurrence_exception_dates) : null, input.source_task_id !== undefined ? input.source_task_id : existing.source_task_id, JSON.stringify(input.metadata ?? existing.metadata), id]);
4507
4586
  return getEvent(id, db);
4508
4587
  }
@@ -4514,21 +4593,22 @@ function deleteEvent(id, db) {
4514
4593
  function findConflicts(calendarId, range, excludeEventId, db) {
4515
4594
  db = db || getDatabase();
4516
4595
  const exclude = excludeEventId ? "AND id != ?" : "";
4517
- const params = excludeEventId ? [calendarId, range.end, range.start, excludeEventId] : [calendarId, range.end, range.start];
4518
- const rows = db.query(`SELECT * FROM events WHERE calendar_id = ? AND start_at < ? AND end_at > ? AND status != 'cancelled' ${exclude} ORDER BY start_at`).all(...params);
4519
- return rows.map(rowToEvent);
4596
+ const params = excludeEventId ? [calendarId, excludeEventId] : [calendarId];
4597
+ const { start: rangeStart, end: rangeEnd } = parseTimeRange(range.start, range.end);
4598
+ const rows = db.query(`SELECT * FROM events WHERE calendar_id = ? AND status != 'cancelled' ${exclude}`).all(...params);
4599
+ return rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at), end: parseEventTimestamp(event.end_at) })).filter(({ start, end }) => start < rangeEnd && end > rangeStart).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
4520
4600
  }
4521
4601
  function searchEvents(query, orgId, db) {
4522
4602
  db = db || getDatabase();
4523
4603
  const rows = db.query(`SELECT e.* FROM events e
4524
4604
  INNER JOIN events_fts f ON f.rowid = e.rowid
4525
4605
  WHERE events_fts MATCH ?
4526
- ${orgId ? "AND e.org_id = ?" : ""}
4527
- ORDER BY e.start_at`).all(query, ...orgId ? [orgId] : []);
4528
- return rows.map(rowToEvent);
4606
+ ${orgId ? "AND e.org_id = ?" : ""}`).all(query, ...orgId ? [orgId] : []);
4607
+ return rows.map(rowToEvent).sort((a, b) => compareEventTimestampStrings(a.start_at, b.start_at));
4529
4608
  }
4530
4609
  var init_events = __esm(() => {
4531
4610
  init_database();
4611
+ init_event_time();
4532
4612
  init_types2();
4533
4613
  });
4534
4614
 
@@ -4574,6 +4654,7 @@ function updateAttendee(id, input, db) {
4574
4654
  }
4575
4655
  var init_attendees = __esm(() => {
4576
4656
  init_database();
4657
+ init_event_time();
4577
4658
  init_types2();
4578
4659
  });
4579
4660
 
@@ -21070,6 +21070,83 @@ var init_calendars = __esm(() => {
21070
21070
  init_types3();
21071
21071
  });
21072
21072
 
21073
+ // src/db/event-time.ts
21074
+ function daysInMonth(year, month) {
21075
+ const date4 = new Date(0);
21076
+ date4.setUTCFullYear(year, month, 0);
21077
+ date4.setUTCHours(0, 0, 0, 0);
21078
+ return date4.getUTCDate();
21079
+ }
21080
+ function timestampBaseMs(year, month, day, hour, minute, second) {
21081
+ const date4 = new Date(0);
21082
+ date4.setUTCFullYear(year, month - 1, day);
21083
+ date4.setUTCHours(hour, minute, second, 0);
21084
+ return date4.getTime();
21085
+ }
21086
+ function parseEventTimestamp(value) {
21087
+ const match = ISO_DATE_TIME_RE.exec(value);
21088
+ if (!match) {
21089
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
21090
+ }
21091
+ const year = Number(match[1]);
21092
+ const month = Number(match[2]);
21093
+ const day = Number(match[3]);
21094
+ const hour = Number(match[4]);
21095
+ const minute = Number(match[5]);
21096
+ const second = Number(match[6]);
21097
+ const fraction = match[7] || "";
21098
+ const offset = match[8];
21099
+ const maxDay = month >= 1 && month <= 12 ? daysInMonth(year, month) : 0;
21100
+ if (month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59) {
21101
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
21102
+ }
21103
+ let offsetMinutes = 0;
21104
+ if (offset !== "Z") {
21105
+ const offsetSign = offset[0] === "-" ? -1 : 1;
21106
+ const offsetHour = Number(offset.slice(1, 3));
21107
+ const offsetMinute = Number(offset.slice(4, 6));
21108
+ if (offsetHour > 23 || offsetMinute > 59) {
21109
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
21110
+ }
21111
+ offsetMinutes = offsetSign * (offsetHour * 60 + offsetMinute);
21112
+ }
21113
+ const timestamp = timestampBaseMs(year, month, day, hour, minute, second);
21114
+ if (!Number.isFinite(timestamp)) {
21115
+ throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
21116
+ }
21117
+ const fractionalNs = BigInt(fraction.padEnd(9, "0") || "0");
21118
+ return BigInt(timestamp) * NS_PER_MS + fractionalNs - BigInt(offsetMinutes) * NS_PER_MINUTE;
21119
+ }
21120
+ function assertEventEndsAfterStart(startAt, endAt) {
21121
+ const start = parseEventTimestamp(startAt);
21122
+ const end = parseEventTimestamp(endAt);
21123
+ if (end <= start) {
21124
+ throw new RangeError("Event end_at must be after start_at");
21125
+ }
21126
+ }
21127
+ function parseTimeRange(startAt, endAt) {
21128
+ const start = parseEventTimestamp(startAt);
21129
+ const end = parseEventTimestamp(endAt);
21130
+ if (end <= start) {
21131
+ throw new RangeError("Time range end must be after start");
21132
+ }
21133
+ return { start, end };
21134
+ }
21135
+ function compareEventInstants(a, b) {
21136
+ if (a < b)
21137
+ return -1;
21138
+ if (a > b)
21139
+ return 1;
21140
+ return 0;
21141
+ }
21142
+ function compareEventTimestampStrings(a, b) {
21143
+ return compareEventInstants(parseEventTimestamp(a), parseEventTimestamp(b)) || a.localeCompare(b);
21144
+ }
21145
+ var ISO_DATE_TIME_RE, NS_PER_MS = 1000000n, NS_PER_MINUTE = 60000000000n;
21146
+ var init_event_time = __esm(() => {
21147
+ ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/;
21148
+ });
21149
+
21073
21150
  // src/db/events.ts
21074
21151
  function rowToEvent(row) {
21075
21152
  return {
@@ -21095,9 +21172,13 @@ function rowToEvent(row) {
21095
21172
  updated_at: row.updated_at
21096
21173
  };
21097
21174
  }
21175
+ function positiveInteger(value) {
21176
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
21177
+ }
21098
21178
  function createEvent(input, db) {
21099
21179
  db = db || getDatabase();
21100
21180
  const id = crypto.randomUUID().slice(0, 8);
21181
+ assertEventEndsAfterStart(input.start_at, input.end_at);
21101
21182
  db.run(`INSERT INTO events (id, calendar_id, org_id, title, description, location, start_at, end_at, all_day, timezone, status, busy_type, visibility, recurrence_rule, recurrence_exception_dates, source_task_id, created_by, metadata)
21102
21183
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.calendar_id, input.org_id, input.title, input.description || null, input.location || null, input.start_at, input.end_at, input.all_day ? 1 : 0, input.timezone || "UTC", input.status || "confirmed", input.busy_type || "busy", input.visibility || "default", input.recurrence_rule || null, input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null, input.source_task_id || null, input.created_by || null, JSON.stringify(input.metadata || {})]);
21103
21184
  return getEvent(id, db);
@@ -21111,6 +21192,8 @@ function listEvents(filter = {}, db) {
21111
21192
  db = db || getDatabase();
21112
21193
  const conditions = [];
21113
21194
  const params = [];
21195
+ const after = filter.after ? parseEventTimestamp(filter.after) : null;
21196
+ const before = filter.before ? parseEventTimestamp(filter.before) : null;
21114
21197
  if (filter.calendar_id) {
21115
21198
  conditions.push("calendar_id = ?");
21116
21199
  params.push(filter.calendar_id);
@@ -21123,14 +21206,6 @@ function listEvents(filter = {}, db) {
21123
21206
  conditions.push("status = ?");
21124
21207
  params.push(filter.status);
21125
21208
  }
21126
- if (filter.after) {
21127
- conditions.push("start_at >= ?");
21128
- params.push(filter.after);
21129
- }
21130
- if (filter.before) {
21131
- conditions.push("start_at <= ?");
21132
- params.push(filter.before);
21133
- }
21134
21209
  if (filter.created_by) {
21135
21210
  conditions.push("created_by = ?");
21136
21211
  params.push(filter.created_by);
@@ -21140,16 +21215,20 @@ function listEvents(filter = {}, db) {
21140
21215
  params.push(filter.source_task_id);
21141
21216
  }
21142
21217
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
21143
- const limit = filter.limit ? `LIMIT ${filter.limit}` : "";
21144
- const offset = filter.offset ? `OFFSET ${filter.offset}` : "";
21145
- const rows = db.query(`SELECT * FROM events ${where} ORDER BY start_at ${limit} ${offset}`).all(...params);
21146
- return rows.map(rowToEvent);
21218
+ const rows = db.query(`SELECT * FROM events ${where}`).all(...params);
21219
+ const events = rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at) })).filter(({ start }) => (after === null || start >= after) && (before === null || start <= before)).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
21220
+ const offset = positiveInteger(filter.offset) || 0;
21221
+ const limit = positiveInteger(filter.limit);
21222
+ return limit ? events.slice(offset, offset + limit) : events.slice(offset);
21147
21223
  }
21148
21224
  function updateEvent(id, input, db) {
21149
21225
  db = db || getDatabase();
21150
21226
  const existing = getEvent(id, db);
21151
21227
  if (!existing)
21152
21228
  throw new NotFoundError("Event", id);
21229
+ const startAt = input.start_at ?? existing.start_at;
21230
+ const endAt = input.end_at ?? existing.end_at;
21231
+ assertEventEndsAfterStart(startAt, endAt);
21153
21232
  db.run(`UPDATE events SET title = ?, description = ?, location = ?, start_at = ?, end_at = ?, all_day = ?, timezone = ?, status = ?, busy_type = ?, visibility = ?, recurrence_rule = ?, recurrence_exception_dates = ?, source_task_id = ?, metadata = ?, updated_at = datetime('now') WHERE id = ?`, [input.title ?? existing.title, input.description !== undefined ? input.description : existing.description, input.location !== undefined ? input.location : existing.location, input.start_at ?? existing.start_at, input.end_at ?? existing.end_at, input.all_day !== undefined ? input.all_day ? 1 : 0 : existing.all_day ? 1 : 0, input.timezone ?? existing.timezone, input.status ?? existing.status, input.busy_type ?? existing.busy_type, input.visibility ?? existing.visibility, input.recurrence_rule !== undefined ? input.recurrence_rule : existing.recurrence_rule, input.recurrence_exception_dates !== undefined ? input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null : existing.recurrence_exception_dates ? JSON.stringify(existing.recurrence_exception_dates) : null, input.source_task_id !== undefined ? input.source_task_id : existing.source_task_id, JSON.stringify(input.metadata ?? existing.metadata), id]);
21154
21233
  return getEvent(id, db);
21155
21234
  }
@@ -21161,21 +21240,22 @@ function deleteEvent(id, db) {
21161
21240
  function findConflicts(calendarId, range, excludeEventId, db) {
21162
21241
  db = db || getDatabase();
21163
21242
  const exclude = excludeEventId ? "AND id != ?" : "";
21164
- const params = excludeEventId ? [calendarId, range.end, range.start, excludeEventId] : [calendarId, range.end, range.start];
21165
- const rows = db.query(`SELECT * FROM events WHERE calendar_id = ? AND start_at < ? AND end_at > ? AND status != 'cancelled' ${exclude} ORDER BY start_at`).all(...params);
21166
- return rows.map(rowToEvent);
21243
+ const params = excludeEventId ? [calendarId, excludeEventId] : [calendarId];
21244
+ const { start: rangeStart, end: rangeEnd } = parseTimeRange(range.start, range.end);
21245
+ const rows = db.query(`SELECT * FROM events WHERE calendar_id = ? AND status != 'cancelled' ${exclude}`).all(...params);
21246
+ return rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at), end: parseEventTimestamp(event.end_at) })).filter(({ start, end }) => start < rangeEnd && end > rangeStart).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
21167
21247
  }
21168
21248
  function searchEvents(query, orgId, db) {
21169
21249
  db = db || getDatabase();
21170
21250
  const rows = db.query(`SELECT e.* FROM events e
21171
21251
  INNER JOIN events_fts f ON f.rowid = e.rowid
21172
21252
  WHERE events_fts MATCH ?
21173
- ${orgId ? "AND e.org_id = ?" : ""}
21174
- ORDER BY e.start_at`).all(query, ...orgId ? [orgId] : []);
21175
- return rows.map(rowToEvent);
21253
+ ${orgId ? "AND e.org_id = ?" : ""}`).all(query, ...orgId ? [orgId] : []);
21254
+ return rows.map(rowToEvent).sort((a, b) => compareEventTimestampStrings(a.start_at, b.start_at));
21176
21255
  }
21177
21256
  var init_events = __esm(() => {
21178
21257
  init_database();
21258
+ init_event_time();
21179
21259
  init_types3();
21180
21260
  });
21181
21261
 
@@ -21221,6 +21301,7 @@ function updateAttendee(id, input, db) {
21221
21301
  }
21222
21302
  var init_attendees = __esm(() => {
21223
21303
  init_database();
21304
+ init_event_time();
21224
21305
  init_types3();
21225
21306
  });
21226
21307
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/calendar",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Universal calendar management for AI coding agents - CLI + MCP server + interactive TUI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",