@hasna/calendar 0.1.8 → 0.1.10

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,32 +3241,22 @@ function deleteCalendar(id, db) {
3241
3241
  return result.changes > 0;
3242
3242
  }
3243
3243
 
3244
- // src/db/events.ts
3245
- function rowToEvent(row) {
3246
- return {
3247
- id: row.id,
3248
- calendar_id: row.calendar_id,
3249
- org_id: row.org_id,
3250
- title: row.title,
3251
- description: row.description,
3252
- location: row.location,
3253
- start_at: row.start_at,
3254
- end_at: row.end_at,
3255
- all_day: !!row.all_day,
3256
- timezone: row.timezone,
3257
- status: row.status,
3258
- busy_type: row.busy_type,
3259
- visibility: row.visibility,
3260
- recurrence_rule: row.recurrence_rule,
3261
- recurrence_exception_dates: row.recurrence_exception_dates ? JSON.parse(row.recurrence_exception_dates) : null,
3262
- source_task_id: row.source_task_id,
3263
- created_by: row.created_by,
3264
- metadata: row.metadata ? JSON.parse(row.metadata) : {},
3265
- created_at: row.created_at,
3266
- updated_at: row.updated_at
3267
- };
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();
3268
3259
  }
3269
- 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})$/;
3270
3260
  function parseEventTimestamp(value) {
3271
3261
  const match = ISO_DATE_TIME_RE.exec(value);
3272
3262
  if (!match) {
@@ -3278,23 +3268,28 @@ function parseEventTimestamp(value) {
3278
3268
  const hour = Number(match[4]);
3279
3269
  const minute = Number(match[5]);
3280
3270
  const second = Number(match[6]);
3281
- const offset = match[7];
3282
- const maxDay = month >= 1 && month <= 12 ? new Date(Date.UTC(year, month, 0)).getUTCDate() : 0;
3271
+ const fraction = match[7] || "";
3272
+ const offset = match[8];
3273
+ const maxDay = month >= 1 && month <= 12 ? daysInMonth(year, month) : 0;
3283
3274
  if (month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59) {
3284
3275
  throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
3285
3276
  }
3277
+ let offsetMinutes = 0;
3286
3278
  if (offset !== "Z") {
3279
+ const offsetSign = offset[0] === "-" ? -1 : 1;
3287
3280
  const offsetHour = Number(offset.slice(1, 3));
3288
3281
  const offsetMinute = Number(offset.slice(4, 6));
3289
3282
  if (offsetHour > 23 || offsetMinute > 59) {
3290
3283
  throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
3291
3284
  }
3285
+ offsetMinutes = offsetSign * (offsetHour * 60 + offsetMinute);
3292
3286
  }
3293
- const timestamp = Date.parse(value);
3287
+ const timestamp = timestampBaseMs(year, month, day, hour, minute, second);
3294
3288
  if (!Number.isFinite(timestamp)) {
3295
3289
  throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
3296
3290
  }
3297
- return timestamp;
3291
+ const fractionalNs = BigInt(fraction.padEnd(9, "0") || "0");
3292
+ return BigInt(timestamp) * NS_PER_MS + fractionalNs - BigInt(offsetMinutes) * NS_PER_MINUTE;
3298
3293
  }
3299
3294
  function assertEventEndsAfterStart(startAt, endAt) {
3300
3295
  const start = parseEventTimestamp(startAt);
@@ -3303,6 +3298,53 @@ function assertEventEndsAfterStart(startAt, endAt) {
3303
3298
  throw new RangeError("Event end_at must be after start_at");
3304
3299
  }
3305
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
+
3320
+ // src/db/events.ts
3321
+ function rowToEvent(row) {
3322
+ return {
3323
+ id: row.id,
3324
+ calendar_id: row.calendar_id,
3325
+ org_id: row.org_id,
3326
+ title: row.title,
3327
+ description: row.description,
3328
+ location: row.location,
3329
+ start_at: row.start_at,
3330
+ end_at: row.end_at,
3331
+ all_day: !!row.all_day,
3332
+ timezone: row.timezone,
3333
+ status: row.status,
3334
+ busy_type: row.busy_type,
3335
+ visibility: row.visibility,
3336
+ recurrence_rule: row.recurrence_rule,
3337
+ recurrence_exception_dates: row.recurrence_exception_dates ? JSON.parse(row.recurrence_exception_dates) : null,
3338
+ source_task_id: row.source_task_id,
3339
+ created_by: row.created_by,
3340
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
3341
+ created_at: row.created_at,
3342
+ updated_at: row.updated_at
3343
+ };
3344
+ }
3345
+ function positiveInteger(value) {
3346
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
3347
+ }
3306
3348
  function createEvent2(input, db) {
3307
3349
  db = db || getDatabase();
3308
3350
  const id = crypto.randomUUID().slice(0, 8);
@@ -3320,6 +3362,8 @@ function listEvents(filter = {}, db) {
3320
3362
  db = db || getDatabase();
3321
3363
  const conditions = [];
3322
3364
  const params = [];
3365
+ const after = filter.after ? parseEventTimestamp(filter.after) : null;
3366
+ const before = filter.before ? parseEventTimestamp(filter.before) : null;
3323
3367
  if (filter.calendar_id) {
3324
3368
  conditions.push("calendar_id = ?");
3325
3369
  params.push(filter.calendar_id);
@@ -3332,14 +3376,6 @@ function listEvents(filter = {}, db) {
3332
3376
  conditions.push("status = ?");
3333
3377
  params.push(filter.status);
3334
3378
  }
3335
- if (filter.after) {
3336
- conditions.push("start_at >= ?");
3337
- params.push(filter.after);
3338
- }
3339
- if (filter.before) {
3340
- conditions.push("start_at <= ?");
3341
- params.push(filter.before);
3342
- }
3343
3379
  if (filter.created_by) {
3344
3380
  conditions.push("created_by = ?");
3345
3381
  params.push(filter.created_by);
@@ -3349,10 +3385,11 @@ function listEvents(filter = {}, db) {
3349
3385
  params.push(filter.source_task_id);
3350
3386
  }
3351
3387
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3352
- const limit = filter.limit ? `LIMIT ${filter.limit}` : "";
3353
- const offset = filter.offset ? `OFFSET ${filter.offset}` : "";
3354
- const rows = db.query(`SELECT * FROM events ${where} ORDER BY start_at ${limit} ${offset}`).all(...params);
3355
- 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);
3356
3393
  }
3357
3394
  function updateEvent(id, input, db) {
3358
3395
  db = db || getDatabase();
@@ -3373,18 +3410,18 @@ function deleteEvent(id, db) {
3373
3410
  function findConflicts(calendarId, range, excludeEventId, db) {
3374
3411
  db = db || getDatabase();
3375
3412
  const exclude = excludeEventId ? "AND id != ?" : "";
3376
- const params = excludeEventId ? [calendarId, range.end, range.start, excludeEventId] : [calendarId, range.end, range.start];
3377
- 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);
3378
- 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);
3379
3417
  }
3380
3418
  function searchEvents(query, orgId, db) {
3381
3419
  db = db || getDatabase();
3382
3420
  const rows = db.query(`SELECT e.* FROM events e
3383
3421
  INNER JOIN events_fts f ON f.rowid = e.rowid
3384
3422
  WHERE events_fts MATCH ?
3385
- ${orgId ? "AND e.org_id = ?" : ""}
3386
- ORDER BY e.start_at`).all(query, ...orgId ? [orgId] : []);
3387
- 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));
3388
3425
  }
3389
3426
 
3390
3427
  // src/db/attendees.ts
@@ -3447,9 +3484,25 @@ function rowToAvailability(row) {
3447
3484
  updated_at: row.updated_at
3448
3485
  };
3449
3486
  }
3487
+ function parseAvailabilityTime(value) {
3488
+ const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value);
3489
+ if (!match) {
3490
+ throw new RangeError("Availability times must use HH:mm format between 00:00 and 23:59");
3491
+ }
3492
+ return Number(match[1]) * 60 + Number(match[2]);
3493
+ }
3494
+ function assertValidAvailabilityWindow(dayOfWeek, startTime, endTime) {
3495
+ if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) {
3496
+ throw new RangeError("Availability day_of_week must be an integer from 0 to 6");
3497
+ }
3498
+ if (parseAvailabilityTime(endTime) <= parseAvailabilityTime(startTime)) {
3499
+ throw new RangeError("Availability end_time must be after start_time");
3500
+ }
3501
+ }
3450
3502
  function createAvailability(input, db) {
3451
3503
  db = db || getDatabase();
3452
3504
  const id = crypto.randomUUID().slice(0, 8);
3505
+ assertValidAvailabilityWindow(input.day_of_week, input.start_time, input.end_time);
3453
3506
  db.run(`INSERT INTO availability (id, agent_id, org_id, day_of_week, start_time, end_time, exceptions) VALUES (?, ?, ?, ?, ?, ?, ?)`, [id, input.agent_id, input.org_id, input.day_of_week, input.start_time, input.end_time, input.exceptions ? JSON.stringify(input.exceptions) : null]);
3454
3507
  return getAvailability(id, db);
3455
3508
  }
@@ -3475,6 +3528,7 @@ function deleteAvailability(id, db) {
3475
3528
  }
3476
3529
  function upsertAgentAvailability(agentId, orgId, dayOfWeek, startTime, endTime, db) {
3477
3530
  db = db || getDatabase();
3531
+ assertValidAvailabilityWindow(dayOfWeek, startTime, endTime);
3478
3532
  const existing = db.query("SELECT * FROM availability WHERE agent_id = ? AND org_id = ? AND day_of_week = ?").all(agentId, orgId, dayOfWeek);
3479
3533
  for (const row of existing) {
3480
3534
  db.run(`DELETE FROM availability WHERE id = ?`, [row.id]);
@@ -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"}
@@ -1 +1 @@
1
- {"version":3,"file":"availability.d.ts","sourceRoot":"","sources":["../../src/db/availability.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,KAAK,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAiB/E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,YAAY,CAU9F;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,YAAY,GAAG,IAAI,CAI9E;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,YAAY,EAAE,CAStG;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CAAE,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,YAAY,CAW7J;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAIrE;AAED,kGAAkG;AAClG,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,YAAY,CAU1J"}
1
+ {"version":3,"file":"availability.d.ts","sourceRoot":"","sources":["../../src/db/availability.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,KAAK,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAoC/E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,YAAY,CAW9F;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,YAAY,GAAG,IAAI,CAI9E;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,YAAY,EAAE,CAStG;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CAAE,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,YAAY,CAc7J;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAIrE;AAED,kGAAkG;AAClG,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,YAAY,CAW1J"}
@@ -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;AA0EnF,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,CAmBhF;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,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,32 +518,22 @@ 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/events.ts
522
- function rowToEvent(row) {
523
- return {
524
- id: row.id,
525
- calendar_id: row.calendar_id,
526
- org_id: row.org_id,
527
- title: row.title,
528
- description: row.description,
529
- location: row.location,
530
- start_at: row.start_at,
531
- end_at: row.end_at,
532
- all_day: !!row.all_day,
533
- timezone: row.timezone,
534
- status: row.status,
535
- busy_type: row.busy_type,
536
- visibility: row.visibility,
537
- recurrence_rule: row.recurrence_rule,
538
- recurrence_exception_dates: row.recurrence_exception_dates ? JSON.parse(row.recurrence_exception_dates) : null,
539
- source_task_id: row.source_task_id,
540
- created_by: row.created_by,
541
- metadata: row.metadata ? JSON.parse(row.metadata) : {},
542
- created_at: row.created_at,
543
- updated_at: row.updated_at
544
- };
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();
545
536
  }
546
- 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})$/;
547
537
  function parseEventTimestamp(value) {
548
538
  const match = ISO_DATE_TIME_RE.exec(value);
549
539
  if (!match) {
@@ -555,23 +545,28 @@ function parseEventTimestamp(value) {
555
545
  const hour = Number(match[4]);
556
546
  const minute = Number(match[5]);
557
547
  const second = Number(match[6]);
558
- const offset = match[7];
559
- const maxDay = month >= 1 && month <= 12 ? new Date(Date.UTC(year, month, 0)).getUTCDate() : 0;
548
+ const fraction = match[7] || "";
549
+ const offset = match[8];
550
+ const maxDay = month >= 1 && month <= 12 ? daysInMonth(year, month) : 0;
560
551
  if (month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59) {
561
552
  throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
562
553
  }
554
+ let offsetMinutes = 0;
563
555
  if (offset !== "Z") {
556
+ const offsetSign = offset[0] === "-" ? -1 : 1;
564
557
  const offsetHour = Number(offset.slice(1, 3));
565
558
  const offsetMinute = Number(offset.slice(4, 6));
566
559
  if (offsetHour > 23 || offsetMinute > 59) {
567
560
  throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
568
561
  }
562
+ offsetMinutes = offsetSign * (offsetHour * 60 + offsetMinute);
569
563
  }
570
- const timestamp = Date.parse(value);
564
+ const timestamp = timestampBaseMs(year, month, day, hour, minute, second);
571
565
  if (!Number.isFinite(timestamp)) {
572
566
  throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
573
567
  }
574
- return timestamp;
568
+ const fractionalNs = BigInt(fraction.padEnd(9, "0") || "0");
569
+ return BigInt(timestamp) * NS_PER_MS + fractionalNs - BigInt(offsetMinutes) * NS_PER_MINUTE;
575
570
  }
576
571
  function assertEventEndsAfterStart(startAt, endAt) {
577
572
  const start = parseEventTimestamp(startAt);
@@ -580,6 +575,53 @@ function assertEventEndsAfterStart(startAt, endAt) {
580
575
  throw new RangeError("Event end_at must be after start_at");
581
576
  }
582
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
+
597
+ // src/db/events.ts
598
+ function rowToEvent(row) {
599
+ return {
600
+ id: row.id,
601
+ calendar_id: row.calendar_id,
602
+ org_id: row.org_id,
603
+ title: row.title,
604
+ description: row.description,
605
+ location: row.location,
606
+ start_at: row.start_at,
607
+ end_at: row.end_at,
608
+ all_day: !!row.all_day,
609
+ timezone: row.timezone,
610
+ status: row.status,
611
+ busy_type: row.busy_type,
612
+ visibility: row.visibility,
613
+ recurrence_rule: row.recurrence_rule,
614
+ recurrence_exception_dates: row.recurrence_exception_dates ? JSON.parse(row.recurrence_exception_dates) : null,
615
+ source_task_id: row.source_task_id,
616
+ created_by: row.created_by,
617
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
618
+ created_at: row.created_at,
619
+ updated_at: row.updated_at
620
+ };
621
+ }
622
+ function positiveInteger(value) {
623
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
624
+ }
583
625
  function createEvent(input, db) {
584
626
  db = db || getDatabase();
585
627
  const id = crypto.randomUUID().slice(0, 8);
@@ -597,6 +639,8 @@ function listEvents(filter = {}, db) {
597
639
  db = db || getDatabase();
598
640
  const conditions = [];
599
641
  const params = [];
642
+ const after = filter.after ? parseEventTimestamp(filter.after) : null;
643
+ const before = filter.before ? parseEventTimestamp(filter.before) : null;
600
644
  if (filter.calendar_id) {
601
645
  conditions.push("calendar_id = ?");
602
646
  params.push(filter.calendar_id);
@@ -609,14 +653,6 @@ function listEvents(filter = {}, db) {
609
653
  conditions.push("status = ?");
610
654
  params.push(filter.status);
611
655
  }
612
- if (filter.after) {
613
- conditions.push("start_at >= ?");
614
- params.push(filter.after);
615
- }
616
- if (filter.before) {
617
- conditions.push("start_at <= ?");
618
- params.push(filter.before);
619
- }
620
656
  if (filter.created_by) {
621
657
  conditions.push("created_by = ?");
622
658
  params.push(filter.created_by);
@@ -626,10 +662,11 @@ function listEvents(filter = {}, db) {
626
662
  params.push(filter.source_task_id);
627
663
  }
628
664
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
629
- const limit = filter.limit ? `LIMIT ${filter.limit}` : "";
630
- const offset = filter.offset ? `OFFSET ${filter.offset}` : "";
631
- const rows = db.query(`SELECT * FROM events ${where} ORDER BY start_at ${limit} ${offset}`).all(...params);
632
- 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);
633
670
  }
634
671
  function updateEvent(id, input, db) {
635
672
  db = db || getDatabase();
@@ -650,28 +687,28 @@ function deleteEvent(id, db) {
650
687
  function findConflicts(calendarId, range, excludeEventId, db) {
651
688
  db = db || getDatabase();
652
689
  const exclude = excludeEventId ? "AND id != ?" : "";
653
- const params = excludeEventId ? [calendarId, range.end, range.start, excludeEventId] : [calendarId, range.end, range.start];
654
- 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);
655
- 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);
656
694
  }
657
695
  function findAgentConflicts(agentId, range, excludeEventId, db) {
658
696
  db = db || getDatabase();
659
697
  const exclude = excludeEventId ? "AND e.id != ?" : "";
660
- 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);
661
700
  const rows = db.query(`SELECT e.* FROM events e
662
701
  INNER JOIN event_attendees a ON a.event_id = e.id
663
- WHERE a.agent_id = ? AND e.start_at < ? AND e.end_at > ? AND e.status != 'cancelled' ${exclude}
664
- ORDER BY e.start_at`).all(...params);
665
- 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);
666
704
  }
667
705
  function searchEvents(query, orgId, db) {
668
706
  db = db || getDatabase();
669
707
  const rows = db.query(`SELECT e.* FROM events e
670
708
  INNER JOIN events_fts f ON f.rowid = e.rowid
671
709
  WHERE events_fts MATCH ?
672
- ${orgId ? "AND e.org_id = ?" : ""}
673
- ORDER BY e.start_at`).all(query, ...orgId ? [orgId] : []);
674
- 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));
675
712
  }
676
713
  // src/db/attendees.ts
677
714
  function rowToAttendee(row) {
@@ -706,10 +743,10 @@ function getAttendeesForEvent(eventId, db) {
706
743
  }
707
744
  function getEventsForAgent(agentId, db) {
708
745
  db = db || getDatabase();
709
- 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
710
747
  INNER JOIN events e ON e.id = a.event_id
711
- WHERE a.agent_id = ? AND e.status != 'cancelled'
712
- 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));
713
750
  return rows.map(rowToAttendee);
714
751
  }
715
752
  function updateAttendee(id, input, db) {
@@ -745,9 +782,25 @@ function rowToAvailability(row) {
745
782
  updated_at: row.updated_at
746
783
  };
747
784
  }
785
+ function parseAvailabilityTime(value) {
786
+ const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value);
787
+ if (!match) {
788
+ throw new RangeError("Availability times must use HH:mm format between 00:00 and 23:59");
789
+ }
790
+ return Number(match[1]) * 60 + Number(match[2]);
791
+ }
792
+ function assertValidAvailabilityWindow(dayOfWeek, startTime, endTime) {
793
+ if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) {
794
+ throw new RangeError("Availability day_of_week must be an integer from 0 to 6");
795
+ }
796
+ if (parseAvailabilityTime(endTime) <= parseAvailabilityTime(startTime)) {
797
+ throw new RangeError("Availability end_time must be after start_time");
798
+ }
799
+ }
748
800
  function createAvailability(input, db) {
749
801
  db = db || getDatabase();
750
802
  const id = crypto.randomUUID().slice(0, 8);
803
+ assertValidAvailabilityWindow(input.day_of_week, input.start_time, input.end_time);
751
804
  db.run(`INSERT INTO availability (id, agent_id, org_id, day_of_week, start_time, end_time, exceptions) VALUES (?, ?, ?, ?, ?, ?, ?)`, [id, input.agent_id, input.org_id, input.day_of_week, input.start_time, input.end_time, input.exceptions ? JSON.stringify(input.exceptions) : null]);
752
805
  return getAvailability(id, db);
753
806
  }
@@ -771,7 +824,10 @@ function updateAvailability(id, updates, db) {
771
824
  const existing = getAvailability(id, db);
772
825
  if (!existing)
773
826
  throw new NotFoundError("Availability", id);
774
- db.run(`UPDATE availability SET start_time = ?, end_time = ?, exceptions = ?, updated_at = datetime('now') WHERE id = ?`, [updates.start_time ?? existing.start_time, updates.end_time ?? existing.end_time, updates.exceptions !== undefined ? updates.exceptions ? JSON.stringify(updates.exceptions) : null : existing.exceptions ? JSON.stringify(existing.exceptions) : null, id]);
827
+ const startTime = updates.start_time ?? existing.start_time;
828
+ const endTime = updates.end_time ?? existing.end_time;
829
+ assertValidAvailabilityWindow(existing.day_of_week, startTime, endTime);
830
+ db.run(`UPDATE availability SET start_time = ?, end_time = ?, exceptions = ?, updated_at = datetime('now') WHERE id = ?`, [startTime, endTime, updates.exceptions !== undefined ? updates.exceptions ? JSON.stringify(updates.exceptions) : null : existing.exceptions ? JSON.stringify(existing.exceptions) : null, id]);
775
831
  return getAvailability(id, db);
776
832
  }
777
833
  function deleteAvailability(id, db) {
@@ -781,6 +837,7 @@ function deleteAvailability(id, db) {
781
837
  }
782
838
  function upsertAgentAvailability(agentId, orgId, dayOfWeek, startTime, endTime, db) {
783
839
  db = db || getDatabase();
840
+ assertValidAvailabilityWindow(dayOfWeek, startTime, endTime);
784
841
  const existing = db.query("SELECT * FROM availability WHERE agent_id = ? AND org_id = ? AND day_of_week = ?").all(agentId, orgId, dayOfWeek);
785
842
  for (const row of existing) {
786
843
  db.run(`DELETE FROM availability WHERE id = ?`, [row.id]);
package/dist/mcp/index.js CHANGED
@@ -4423,30 +4423,18 @@ var init_calendars = __esm(() => {
4423
4423
  init_types2();
4424
4424
  });
4425
4425
 
4426
- // src/db/events.ts
4427
- function rowToEvent(row) {
4428
- return {
4429
- id: row.id,
4430
- calendar_id: row.calendar_id,
4431
- org_id: row.org_id,
4432
- title: row.title,
4433
- description: row.description,
4434
- location: row.location,
4435
- start_at: row.start_at,
4436
- end_at: row.end_at,
4437
- all_day: !!row.all_day,
4438
- timezone: row.timezone,
4439
- status: row.status,
4440
- busy_type: row.busy_type,
4441
- visibility: row.visibility,
4442
- recurrence_rule: row.recurrence_rule,
4443
- recurrence_exception_dates: row.recurrence_exception_dates ? JSON.parse(row.recurrence_exception_dates) : null,
4444
- source_task_id: row.source_task_id,
4445
- created_by: row.created_by,
4446
- metadata: row.metadata ? JSON.parse(row.metadata) : {},
4447
- created_at: row.created_at,
4448
- updated_at: row.updated_at
4449
- };
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();
4450
4438
  }
4451
4439
  function parseEventTimestamp(value) {
4452
4440
  const match = ISO_DATE_TIME_RE.exec(value);
@@ -4459,23 +4447,28 @@ function parseEventTimestamp(value) {
4459
4447
  const hour = Number(match[4]);
4460
4448
  const minute = Number(match[5]);
4461
4449
  const second = Number(match[6]);
4462
- const offset = match[7];
4463
- const maxDay = month >= 1 && month <= 12 ? new Date(Date.UTC(year, month, 0)).getUTCDate() : 0;
4450
+ const fraction = match[7] || "";
4451
+ const offset = match[8];
4452
+ const maxDay = month >= 1 && month <= 12 ? daysInMonth(year, month) : 0;
4464
4453
  if (month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59) {
4465
4454
  throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
4466
4455
  }
4456
+ let offsetMinutes = 0;
4467
4457
  if (offset !== "Z") {
4458
+ const offsetSign = offset[0] === "-" ? -1 : 1;
4468
4459
  const offsetHour = Number(offset.slice(1, 3));
4469
4460
  const offsetMinute = Number(offset.slice(4, 6));
4470
4461
  if (offsetHour > 23 || offsetMinute > 59) {
4471
4462
  throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
4472
4463
  }
4464
+ offsetMinutes = offsetSign * (offsetHour * 60 + offsetMinute);
4473
4465
  }
4474
- const timestamp = Date.parse(value);
4466
+ const timestamp = timestampBaseMs(year, month, day, hour, minute, second);
4475
4467
  if (!Number.isFinite(timestamp)) {
4476
4468
  throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
4477
4469
  }
4478
- return timestamp;
4470
+ const fractionalNs = BigInt(fraction.padEnd(9, "0") || "0");
4471
+ return BigInt(timestamp) * NS_PER_MS + fractionalNs - BigInt(offsetMinutes) * NS_PER_MINUTE;
4479
4472
  }
4480
4473
  function assertEventEndsAfterStart(startAt, endAt) {
4481
4474
  const start = parseEventTimestamp(startAt);
@@ -4484,6 +4477,57 @@ function assertEventEndsAfterStart(startAt, endAt) {
4484
4477
  throw new RangeError("Event end_at must be after start_at");
4485
4478
  }
4486
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
+
4503
+ // src/db/events.ts
4504
+ function rowToEvent(row) {
4505
+ return {
4506
+ id: row.id,
4507
+ calendar_id: row.calendar_id,
4508
+ org_id: row.org_id,
4509
+ title: row.title,
4510
+ description: row.description,
4511
+ location: row.location,
4512
+ start_at: row.start_at,
4513
+ end_at: row.end_at,
4514
+ all_day: !!row.all_day,
4515
+ timezone: row.timezone,
4516
+ status: row.status,
4517
+ busy_type: row.busy_type,
4518
+ visibility: row.visibility,
4519
+ recurrence_rule: row.recurrence_rule,
4520
+ recurrence_exception_dates: row.recurrence_exception_dates ? JSON.parse(row.recurrence_exception_dates) : null,
4521
+ source_task_id: row.source_task_id,
4522
+ created_by: row.created_by,
4523
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
4524
+ created_at: row.created_at,
4525
+ updated_at: row.updated_at
4526
+ };
4527
+ }
4528
+ function positiveInteger(value) {
4529
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
4530
+ }
4487
4531
  function createEvent(input, db) {
4488
4532
  db = db || getDatabase();
4489
4533
  const id = crypto.randomUUID().slice(0, 8);
@@ -4501,6 +4545,8 @@ function listEvents(filter = {}, db) {
4501
4545
  db = db || getDatabase();
4502
4546
  const conditions = [];
4503
4547
  const params = [];
4548
+ const after = filter.after ? parseEventTimestamp(filter.after) : null;
4549
+ const before = filter.before ? parseEventTimestamp(filter.before) : null;
4504
4550
  if (filter.calendar_id) {
4505
4551
  conditions.push("calendar_id = ?");
4506
4552
  params.push(filter.calendar_id);
@@ -4513,14 +4559,6 @@ function listEvents(filter = {}, db) {
4513
4559
  conditions.push("status = ?");
4514
4560
  params.push(filter.status);
4515
4561
  }
4516
- if (filter.after) {
4517
- conditions.push("start_at >= ?");
4518
- params.push(filter.after);
4519
- }
4520
- if (filter.before) {
4521
- conditions.push("start_at <= ?");
4522
- params.push(filter.before);
4523
- }
4524
4562
  if (filter.created_by) {
4525
4563
  conditions.push("created_by = ?");
4526
4564
  params.push(filter.created_by);
@@ -4530,10 +4568,11 @@ function listEvents(filter = {}, db) {
4530
4568
  params.push(filter.source_task_id);
4531
4569
  }
4532
4570
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4533
- const limit = filter.limit ? `LIMIT ${filter.limit}` : "";
4534
- const offset = filter.offset ? `OFFSET ${filter.offset}` : "";
4535
- const rows = db.query(`SELECT * FROM events ${where} ORDER BY start_at ${limit} ${offset}`).all(...params);
4536
- 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);
4537
4576
  }
4538
4577
  function updateEvent(id, input, db) {
4539
4578
  db = db || getDatabase();
@@ -4554,24 +4593,23 @@ function deleteEvent(id, db) {
4554
4593
  function findConflicts(calendarId, range, excludeEventId, db) {
4555
4594
  db = db || getDatabase();
4556
4595
  const exclude = excludeEventId ? "AND id != ?" : "";
4557
- const params = excludeEventId ? [calendarId, range.end, range.start, excludeEventId] : [calendarId, range.end, range.start];
4558
- 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);
4559
- 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);
4560
4600
  }
4561
4601
  function searchEvents(query, orgId, db) {
4562
4602
  db = db || getDatabase();
4563
4603
  const rows = db.query(`SELECT e.* FROM events e
4564
4604
  INNER JOIN events_fts f ON f.rowid = e.rowid
4565
4605
  WHERE events_fts MATCH ?
4566
- ${orgId ? "AND e.org_id = ?" : ""}
4567
- ORDER BY e.start_at`).all(query, ...orgId ? [orgId] : []);
4568
- 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));
4569
4608
  }
4570
- var ISO_DATE_TIME_RE;
4571
4609
  var init_events = __esm(() => {
4572
4610
  init_database();
4611
+ init_event_time();
4573
4612
  init_types2();
4574
- 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})$/;
4575
4613
  });
4576
4614
 
4577
4615
  // src/db/attendees.ts
@@ -4616,6 +4654,7 @@ function updateAttendee(id, input, db) {
4616
4654
  }
4617
4655
  var init_attendees = __esm(() => {
4618
4656
  init_database();
4657
+ init_event_time();
4619
4658
  init_types2();
4620
4659
  });
4621
4660
 
@@ -4633,9 +4672,25 @@ function rowToAvailability(row) {
4633
4672
  updated_at: row.updated_at
4634
4673
  };
4635
4674
  }
4675
+ function parseAvailabilityTime(value) {
4676
+ const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value);
4677
+ if (!match) {
4678
+ throw new RangeError("Availability times must use HH:mm format between 00:00 and 23:59");
4679
+ }
4680
+ return Number(match[1]) * 60 + Number(match[2]);
4681
+ }
4682
+ function assertValidAvailabilityWindow(dayOfWeek, startTime, endTime) {
4683
+ if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) {
4684
+ throw new RangeError("Availability day_of_week must be an integer from 0 to 6");
4685
+ }
4686
+ if (parseAvailabilityTime(endTime) <= parseAvailabilityTime(startTime)) {
4687
+ throw new RangeError("Availability end_time must be after start_time");
4688
+ }
4689
+ }
4636
4690
  function createAvailability(input, db) {
4637
4691
  db = db || getDatabase();
4638
4692
  const id = crypto.randomUUID().slice(0, 8);
4693
+ assertValidAvailabilityWindow(input.day_of_week, input.start_time, input.end_time);
4639
4694
  db.run(`INSERT INTO availability (id, agent_id, org_id, day_of_week, start_time, end_time, exceptions) VALUES (?, ?, ?, ?, ?, ?, ?)`, [id, input.agent_id, input.org_id, input.day_of_week, input.start_time, input.end_time, input.exceptions ? JSON.stringify(input.exceptions) : null]);
4640
4695
  return getAvailability(id, db);
4641
4696
  }
@@ -4656,6 +4711,7 @@ function getAvailabilityForAgent(agentId, orgId, db) {
4656
4711
  }
4657
4712
  function upsertAgentAvailability(agentId, orgId, dayOfWeek, startTime, endTime, db) {
4658
4713
  db = db || getDatabase();
4714
+ assertValidAvailabilityWindow(dayOfWeek, startTime, endTime);
4659
4715
  const existing = db.query("SELECT * FROM availability WHERE agent_id = ? AND org_id = ? AND day_of_week = ?").all(agentId, orgId, dayOfWeek);
4660
4716
  for (const row of existing) {
4661
4717
  db.run(`DELETE FROM availability WHERE id = ?`, [row.id]);
@@ -21070,30 +21070,18 @@ var init_calendars = __esm(() => {
21070
21070
  init_types3();
21071
21071
  });
21072
21072
 
21073
- // src/db/events.ts
21074
- function rowToEvent(row) {
21075
- return {
21076
- id: row.id,
21077
- calendar_id: row.calendar_id,
21078
- org_id: row.org_id,
21079
- title: row.title,
21080
- description: row.description,
21081
- location: row.location,
21082
- start_at: row.start_at,
21083
- end_at: row.end_at,
21084
- all_day: !!row.all_day,
21085
- timezone: row.timezone,
21086
- status: row.status,
21087
- busy_type: row.busy_type,
21088
- visibility: row.visibility,
21089
- recurrence_rule: row.recurrence_rule,
21090
- recurrence_exception_dates: row.recurrence_exception_dates ? JSON.parse(row.recurrence_exception_dates) : null,
21091
- source_task_id: row.source_task_id,
21092
- created_by: row.created_by,
21093
- metadata: row.metadata ? JSON.parse(row.metadata) : {},
21094
- created_at: row.created_at,
21095
- updated_at: row.updated_at
21096
- };
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();
21097
21085
  }
21098
21086
  function parseEventTimestamp(value) {
21099
21087
  const match = ISO_DATE_TIME_RE.exec(value);
@@ -21106,23 +21094,28 @@ function parseEventTimestamp(value) {
21106
21094
  const hour = Number(match[4]);
21107
21095
  const minute = Number(match[5]);
21108
21096
  const second = Number(match[6]);
21109
- const offset = match[7];
21110
- const maxDay = month >= 1 && month <= 12 ? new Date(Date.UTC(year, month, 0)).getUTCDate() : 0;
21097
+ const fraction = match[7] || "";
21098
+ const offset = match[8];
21099
+ const maxDay = month >= 1 && month <= 12 ? daysInMonth(year, month) : 0;
21111
21100
  if (month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59) {
21112
21101
  throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
21113
21102
  }
21103
+ let offsetMinutes = 0;
21114
21104
  if (offset !== "Z") {
21105
+ const offsetSign = offset[0] === "-" ? -1 : 1;
21115
21106
  const offsetHour = Number(offset.slice(1, 3));
21116
21107
  const offsetMinute = Number(offset.slice(4, 6));
21117
21108
  if (offsetHour > 23 || offsetMinute > 59) {
21118
21109
  throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
21119
21110
  }
21111
+ offsetMinutes = offsetSign * (offsetHour * 60 + offsetMinute);
21120
21112
  }
21121
- const timestamp = Date.parse(value);
21113
+ const timestamp = timestampBaseMs(year, month, day, hour, minute, second);
21122
21114
  if (!Number.isFinite(timestamp)) {
21123
21115
  throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
21124
21116
  }
21125
- return timestamp;
21117
+ const fractionalNs = BigInt(fraction.padEnd(9, "0") || "0");
21118
+ return BigInt(timestamp) * NS_PER_MS + fractionalNs - BigInt(offsetMinutes) * NS_PER_MINUTE;
21126
21119
  }
21127
21120
  function assertEventEndsAfterStart(startAt, endAt) {
21128
21121
  const start = parseEventTimestamp(startAt);
@@ -21131,6 +21124,57 @@ function assertEventEndsAfterStart(startAt, endAt) {
21131
21124
  throw new RangeError("Event end_at must be after start_at");
21132
21125
  }
21133
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
+
21150
+ // src/db/events.ts
21151
+ function rowToEvent(row) {
21152
+ return {
21153
+ id: row.id,
21154
+ calendar_id: row.calendar_id,
21155
+ org_id: row.org_id,
21156
+ title: row.title,
21157
+ description: row.description,
21158
+ location: row.location,
21159
+ start_at: row.start_at,
21160
+ end_at: row.end_at,
21161
+ all_day: !!row.all_day,
21162
+ timezone: row.timezone,
21163
+ status: row.status,
21164
+ busy_type: row.busy_type,
21165
+ visibility: row.visibility,
21166
+ recurrence_rule: row.recurrence_rule,
21167
+ recurrence_exception_dates: row.recurrence_exception_dates ? JSON.parse(row.recurrence_exception_dates) : null,
21168
+ source_task_id: row.source_task_id,
21169
+ created_by: row.created_by,
21170
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
21171
+ created_at: row.created_at,
21172
+ updated_at: row.updated_at
21173
+ };
21174
+ }
21175
+ function positiveInteger(value) {
21176
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
21177
+ }
21134
21178
  function createEvent(input, db) {
21135
21179
  db = db || getDatabase();
21136
21180
  const id = crypto.randomUUID().slice(0, 8);
@@ -21148,6 +21192,8 @@ function listEvents(filter = {}, db) {
21148
21192
  db = db || getDatabase();
21149
21193
  const conditions = [];
21150
21194
  const params = [];
21195
+ const after = filter.after ? parseEventTimestamp(filter.after) : null;
21196
+ const before = filter.before ? parseEventTimestamp(filter.before) : null;
21151
21197
  if (filter.calendar_id) {
21152
21198
  conditions.push("calendar_id = ?");
21153
21199
  params.push(filter.calendar_id);
@@ -21160,14 +21206,6 @@ function listEvents(filter = {}, db) {
21160
21206
  conditions.push("status = ?");
21161
21207
  params.push(filter.status);
21162
21208
  }
21163
- if (filter.after) {
21164
- conditions.push("start_at >= ?");
21165
- params.push(filter.after);
21166
- }
21167
- if (filter.before) {
21168
- conditions.push("start_at <= ?");
21169
- params.push(filter.before);
21170
- }
21171
21209
  if (filter.created_by) {
21172
21210
  conditions.push("created_by = ?");
21173
21211
  params.push(filter.created_by);
@@ -21177,10 +21215,11 @@ function listEvents(filter = {}, db) {
21177
21215
  params.push(filter.source_task_id);
21178
21216
  }
21179
21217
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
21180
- const limit = filter.limit ? `LIMIT ${filter.limit}` : "";
21181
- const offset = filter.offset ? `OFFSET ${filter.offset}` : "";
21182
- const rows = db.query(`SELECT * FROM events ${where} ORDER BY start_at ${limit} ${offset}`).all(...params);
21183
- 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);
21184
21223
  }
21185
21224
  function updateEvent(id, input, db) {
21186
21225
  db = db || getDatabase();
@@ -21201,24 +21240,23 @@ function deleteEvent(id, db) {
21201
21240
  function findConflicts(calendarId, range, excludeEventId, db) {
21202
21241
  db = db || getDatabase();
21203
21242
  const exclude = excludeEventId ? "AND id != ?" : "";
21204
- const params = excludeEventId ? [calendarId, range.end, range.start, excludeEventId] : [calendarId, range.end, range.start];
21205
- 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);
21206
- 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);
21207
21247
  }
21208
21248
  function searchEvents(query, orgId, db) {
21209
21249
  db = db || getDatabase();
21210
21250
  const rows = db.query(`SELECT e.* FROM events e
21211
21251
  INNER JOIN events_fts f ON f.rowid = e.rowid
21212
21252
  WHERE events_fts MATCH ?
21213
- ${orgId ? "AND e.org_id = ?" : ""}
21214
- ORDER BY e.start_at`).all(query, ...orgId ? [orgId] : []);
21215
- 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));
21216
21255
  }
21217
- var ISO_DATE_TIME_RE;
21218
21256
  var init_events = __esm(() => {
21219
21257
  init_database();
21258
+ init_event_time();
21220
21259
  init_types3();
21221
- 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})$/;
21222
21260
  });
21223
21261
 
21224
21262
  // src/db/attendees.ts
@@ -21263,6 +21301,7 @@ function updateAttendee(id, input, db) {
21263
21301
  }
21264
21302
  var init_attendees = __esm(() => {
21265
21303
  init_database();
21304
+ init_event_time();
21266
21305
  init_types3();
21267
21306
  });
21268
21307
 
@@ -21280,9 +21319,25 @@ function rowToAvailability(row) {
21280
21319
  updated_at: row.updated_at
21281
21320
  };
21282
21321
  }
21322
+ function parseAvailabilityTime(value) {
21323
+ const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value);
21324
+ if (!match) {
21325
+ throw new RangeError("Availability times must use HH:mm format between 00:00 and 23:59");
21326
+ }
21327
+ return Number(match[1]) * 60 + Number(match[2]);
21328
+ }
21329
+ function assertValidAvailabilityWindow(dayOfWeek, startTime, endTime) {
21330
+ if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) {
21331
+ throw new RangeError("Availability day_of_week must be an integer from 0 to 6");
21332
+ }
21333
+ if (parseAvailabilityTime(endTime) <= parseAvailabilityTime(startTime)) {
21334
+ throw new RangeError("Availability end_time must be after start_time");
21335
+ }
21336
+ }
21283
21337
  function createAvailability(input, db) {
21284
21338
  db = db || getDatabase();
21285
21339
  const id = crypto.randomUUID().slice(0, 8);
21340
+ assertValidAvailabilityWindow(input.day_of_week, input.start_time, input.end_time);
21286
21341
  db.run(`INSERT INTO availability (id, agent_id, org_id, day_of_week, start_time, end_time, exceptions) VALUES (?, ?, ?, ?, ?, ?, ?)`, [id, input.agent_id, input.org_id, input.day_of_week, input.start_time, input.end_time, input.exceptions ? JSON.stringify(input.exceptions) : null]);
21287
21342
  return getAvailability(id, db);
21288
21343
  }
@@ -21303,6 +21358,7 @@ function getAvailabilityForAgent(agentId, orgId, db) {
21303
21358
  }
21304
21359
  function upsertAgentAvailability(agentId, orgId, dayOfWeek, startTime, endTime, db) {
21305
21360
  db = db || getDatabase();
21361
+ assertValidAvailabilityWindow(dayOfWeek, startTime, endTime);
21306
21362
  const existing = db.query("SELECT * FROM availability WHERE agent_id = ? AND org_id = ? AND day_of_week = ?").all(agentId, orgId, dayOfWeek);
21307
21363
  for (const row of existing) {
21308
21364
  db.run(`DELETE FROM availability WHERE id = ?`, [row.id]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/calendar",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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",