feat: add generic CalDAV calendar data source (#42)

* feat: add generic CalDAV calendar data source

Add @aris/source-caldav package that fetches calendar events from any
CalDAV server via tsdav + ical.js.

- Supports Basic auth and OAuth via explicit authMethod discriminant
- serverUrl provided at construction time, not hardcoded
- Optional timeZone for correct local day boundaries
- Credentials cleared from memory after client login
- Failed calendar fetches logged, not silently dropped
- Login promise cached with retry on failure

Co-authored-by: Ona <no-reply@ona.com>

* fix: deduplicate concurrent fetchEvents calls

Co-authored-by: Ona <no-reply@ona.com>

* fix: timezone-aware signals, low-priority cancelled events

- computeSignals uses startOfDay(timeZone) for 'later today' boundary
- Cancelled events get urgency 0.1, excluded from context inProgress/nextEvent

Co-authored-by: Ona <no-reply@ona.com>

---------

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
2026-02-28 16:09:11 +00:00
committed by GitHub
parent 82ac2b577d
commit 40ad90aa2d
16 changed files with 1485 additions and 1 deletions

View File

@@ -0,0 +1,62 @@
/**
* Live test script for CalDavSource.
*
* Usage:
* bun run test-live.ts
*/
import { CalDavSource } from "../src/index.ts"
const serverUrl = prompt("CalDAV server URL:")
const username = prompt("Username:")
const password = prompt("Password:")
const lookAheadRaw = prompt("Look-ahead days (default 0):")
if (!serverUrl || !username || !password) {
console.error("Server URL, username, and password are required.")
process.exit(1)
}
const lookAheadDays = Number(lookAheadRaw) || 0
const source = new CalDavSource({
serverUrl,
authMethod: "basic",
username,
password,
lookAheadDays,
})
const context = { time: new Date() }
console.log(`\nFetching from ${serverUrl} as ${username} (lookAheadDays=${lookAheadDays})...\n`)
const contextResult = await source.fetchContext(context)
const items = await source.fetchItems(context)
console.log("=== Context ===")
console.log(JSON.stringify(contextResult, null, 2))
console.log(`\n=== Feed Items (${items.length}) ===`)
for (const item of items) {
console.log(`\n--- ${item.data.title} ---`)
console.log(` ID: ${item.id}`)
console.log(` Calendar: ${item.data.calendarName ?? "(unknown)"}`)
console.log(` Start: ${item.data.startDate.toISOString()}`)
console.log(` End: ${item.data.endDate.toISOString()}`)
console.log(` All-day: ${item.data.isAllDay}`)
console.log(` Location: ${item.data.location ?? "(none)"}`)
console.log(` Status: ${item.data.status ?? "(none)"}`)
console.log(` Urgency: ${item.signals?.urgency}`)
console.log(` Relevance: ${item.signals?.timeRelevance}`)
if (item.data.attendees.length > 0) {
console.log(` Attendees: ${item.data.attendees.map((a) => a.name ?? a.email).join(", ")}`)
}
if (item.data.description) {
console.log(` Desc: ${item.data.description.slice(0, 100)}`)
}
}
if (items.length === 0) {
console.log("(no events found in the time window)")
}