@hasna/calendar 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,862 @@
1
+ // @bun
2
+ // src/types/index.ts
3
+ class NotFoundError extends Error {
4
+ entityType;
5
+ entityId;
6
+ static code = "NOT_FOUND";
7
+ constructor(entityType, entityId) {
8
+ super(`${entityType} not found: ${entityId}`);
9
+ this.entityType = entityType;
10
+ this.entityId = entityId;
11
+ this.name = "NotFoundError";
12
+ }
13
+ }
14
+
15
+ class ConflictError extends Error {
16
+ static code = "CONFLICT";
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "ConflictError";
20
+ }
21
+ }
22
+ // src/db/database.ts
23
+ import { Database } from "bun:sqlite";
24
+ import { existsSync, mkdirSync, unlinkSync } from "fs";
25
+ import { dirname, join, resolve } from "path";
26
+ function isInMemoryDb(path) {
27
+ return path === ":memory:" || path.startsWith("file::memory:");
28
+ }
29
+ function findNearestCalendarDb(startDir) {
30
+ let dir = resolve(startDir);
31
+ while (true) {
32
+ const candidate = join(dir, ".calendar", "calendar.db");
33
+ if (existsSync(candidate))
34
+ return candidate;
35
+ const parent = dirname(dir);
36
+ if (parent === dir)
37
+ break;
38
+ dir = parent;
39
+ }
40
+ return null;
41
+ }
42
+ function findGitRoot(startDir) {
43
+ let dir = resolve(startDir);
44
+ while (true) {
45
+ if (existsSync(join(dir, ".git")))
46
+ return dir;
47
+ const parent = dirname(dir);
48
+ if (parent === dir)
49
+ break;
50
+ dir = parent;
51
+ }
52
+ return null;
53
+ }
54
+ function getDbPath() {
55
+ if (process.env["BUN_TEST"]) {
56
+ return ":memory:";
57
+ }
58
+ if (process.env["CALENDAR_DB_PATH"]) {
59
+ return process.env["CALENDAR_DB_PATH"];
60
+ }
61
+ const cwd = process.cwd();
62
+ const nearest = findNearestCalendarDb(cwd);
63
+ if (nearest)
64
+ return nearest;
65
+ if (process.env["CALENDAR_DB_SCOPE"] === "project") {
66
+ const gitRoot = findGitRoot(cwd);
67
+ if (gitRoot) {
68
+ return join(gitRoot, ".calendar", "calendar.db");
69
+ }
70
+ }
71
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
72
+ const newPath = join(home, ".hasna", "calendar", "calendar.db");
73
+ const legacyPath = join(home, ".calendar", "calendar.db");
74
+ if (!existsSync(newPath) && existsSync(legacyPath)) {
75
+ return legacyPath;
76
+ }
77
+ return newPath;
78
+ }
79
+ function ensureDir(filePath) {
80
+ if (isInMemoryDb(filePath))
81
+ return;
82
+ const dir = dirname(resolve(filePath));
83
+ if (!existsSync(dir)) {
84
+ mkdirSync(dir, { recursive: true });
85
+ }
86
+ }
87
+ var _db = null;
88
+ function getDatabase(dbPath) {
89
+ if (_db)
90
+ return _db;
91
+ const path = dbPath || getDbPath();
92
+ ensureDir(path);
93
+ _db = new Database(path);
94
+ _db.run("PRAGMA journal_mode = WAL");
95
+ _db.run("PRAGMA busy_timeout = 5000");
96
+ _db.run("PRAGMA foreign_keys = ON");
97
+ runMigrations(_db);
98
+ return _db;
99
+ }
100
+ function closeDatabase() {
101
+ if (_db) {
102
+ _db.close();
103
+ _db = null;
104
+ }
105
+ }
106
+ var migrations = [
107
+ {
108
+ id: 1,
109
+ up: (db) => {
110
+ db.run(`CREATE TABLE IF NOT EXISTS _migrations (
111
+ id INTEGER PRIMARY KEY,
112
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
113
+ )`);
114
+ db.run(`CREATE TABLE IF NOT EXISTS orgs (
115
+ id TEXT PRIMARY KEY,
116
+ name TEXT NOT NULL,
117
+ slug TEXT UNIQUE NOT NULL,
118
+ description TEXT,
119
+ metadata TEXT NOT NULL DEFAULT '{}',
120
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
121
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
122
+ )`);
123
+ db.run(`CREATE TABLE IF NOT EXISTS agents (
124
+ id TEXT PRIMARY KEY,
125
+ name TEXT UNIQUE NOT NULL,
126
+ description TEXT,
127
+ role TEXT,
128
+ title TEXT,
129
+ level TEXT,
130
+ capabilities TEXT NOT NULL DEFAULT '[]',
131
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived')),
132
+ metadata TEXT NOT NULL DEFAULT '{}',
133
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
134
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
135
+ session_id TEXT,
136
+ working_dir TEXT,
137
+ active_org_id TEXT,
138
+ FOREIGN KEY (active_org_id) REFERENCES orgs(id) ON DELETE SET NULL
139
+ )`);
140
+ db.run(`CREATE TABLE IF NOT EXISTS org_memberships (
141
+ id TEXT PRIMARY KEY,
142
+ org_id TEXT NOT NULL,
143
+ agent_id TEXT NOT NULL,
144
+ role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('admin', 'member', 'service')),
145
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
146
+ UNIQUE(org_id, agent_id),
147
+ FOREIGN KEY (org_id) REFERENCES orgs(id) ON DELETE CASCADE,
148
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
149
+ )`);
150
+ db.run(`CREATE TABLE IF NOT EXISTS calendars (
151
+ id TEXT PRIMARY KEY,
152
+ org_id TEXT NOT NULL,
153
+ owner_id TEXT,
154
+ slug TEXT NOT NULL,
155
+ name TEXT NOT NULL,
156
+ description TEXT,
157
+ color TEXT,
158
+ timezone TEXT NOT NULL DEFAULT 'UTC',
159
+ visibility TEXT NOT NULL DEFAULT 'org' CHECK(visibility IN ('public', 'org', 'private')),
160
+ metadata TEXT NOT NULL DEFAULT '{}',
161
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
162
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
163
+ UNIQUE(org_id, slug),
164
+ FOREIGN KEY (org_id) REFERENCES orgs(id) ON DELETE CASCADE,
165
+ FOREIGN KEY (owner_id) REFERENCES agents(id) ON DELETE SET NULL
166
+ )`);
167
+ db.run(`CREATE TABLE IF NOT EXISTS events (
168
+ id TEXT PRIMARY KEY,
169
+ calendar_id TEXT NOT NULL,
170
+ org_id TEXT NOT NULL,
171
+ title TEXT NOT NULL,
172
+ description TEXT,
173
+ location TEXT,
174
+ start_at TEXT NOT NULL,
175
+ end_at TEXT NOT NULL,
176
+ all_day INTEGER NOT NULL DEFAULT 0,
177
+ timezone TEXT NOT NULL DEFAULT 'UTC',
178
+ status TEXT NOT NULL DEFAULT 'confirmed' CHECK(status IN ('tentative', 'confirmed', 'cancelled')),
179
+ busy_type TEXT NOT NULL DEFAULT 'busy' CHECK(busy_type IN ('busy', 'free', 'out_of_office')),
180
+ visibility TEXT NOT NULL DEFAULT 'default' CHECK(visibility IN ('default', 'private', 'confidential')),
181
+ recurrence_rule TEXT,
182
+ recurrence_exception_dates TEXT,
183
+ source_task_id TEXT,
184
+ created_by TEXT,
185
+ metadata TEXT NOT NULL DEFAULT '{}',
186
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
187
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
188
+ FOREIGN KEY (calendar_id) REFERENCES calendars(id) ON DELETE CASCADE,
189
+ FOREIGN KEY (org_id) REFERENCES orgs(id) ON DELETE CASCADE,
190
+ FOREIGN KEY (created_by) REFERENCES agents(id) ON DELETE SET NULL
191
+ )`);
192
+ db.run(`CREATE TABLE IF NOT EXISTS event_attendees (
193
+ id TEXT PRIMARY KEY,
194
+ event_id TEXT NOT NULL,
195
+ agent_id TEXT,
196
+ display_name TEXT,
197
+ email TEXT,
198
+ status TEXT NOT NULL DEFAULT 'needsAction' CHECK(status IN ('needsAction', 'accepted', 'declined', 'tentative')),
199
+ required INTEGER NOT NULL DEFAULT 1,
200
+ response_comment TEXT,
201
+ responded_at TEXT,
202
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
203
+ FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE,
204
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
205
+ )`);
206
+ db.run(`CREATE TABLE IF NOT EXISTS availability (
207
+ id TEXT PRIMARY KEY,
208
+ agent_id TEXT NOT NULL,
209
+ org_id TEXT NOT NULL,
210
+ day_of_week INTEGER NOT NULL CHECK(day_of_week BETWEEN 0 AND 6),
211
+ start_time TEXT NOT NULL,
212
+ end_time TEXT NOT NULL,
213
+ exceptions TEXT,
214
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
215
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
216
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE,
217
+ FOREIGN KEY (org_id) REFERENCES orgs(id) ON DELETE CASCADE
218
+ )`);
219
+ db.run(`CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
220
+ title, description, location,
221
+ content='events',
222
+ content_rowid='rowid'
223
+ )`);
224
+ db.run(`CREATE TRIGGER IF NOT EXISTS events_ai AFTER INSERT ON events BEGIN
225
+ INSERT INTO events_fts(rowid, title, description, location)
226
+ VALUES (new.rowid, new.title, new.description, new.location);
227
+ END`);
228
+ db.run(`CREATE TRIGGER IF NOT EXISTS events_ad AFTER DELETE ON events BEGIN
229
+ INSERT INTO events_fts(events_fts, rowid, title, description, location)
230
+ VALUES ('delete', old.rowid, old.title, old.description, old.location);
231
+ END`);
232
+ db.run(`CREATE TRIGGER IF NOT EXISTS events_au AFTER UPDATE ON events BEGIN
233
+ INSERT INTO events_fts(events_fts, rowid, title, description, location)
234
+ VALUES ('delete', old.rowid, old.title, old.description, old.location);
235
+ INSERT INTO events_fts(rowid, title, description, location)
236
+ VALUES (new.rowid, new.title, new.description, new.location);
237
+ END`);
238
+ }
239
+ }
240
+ ];
241
+ function runMigrations(db) {
242
+ db.run("CREATE TABLE IF NOT EXISTS _migrations (id INTEGER PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (datetime('now')))");
243
+ const applied = db.query("SELECT id FROM _migrations ORDER BY id").all();
244
+ const appliedIds = new Set(applied.map((r) => r.id));
245
+ for (const migration of migrations) {
246
+ if (!appliedIds.has(migration.id)) {
247
+ migration.up(db);
248
+ db.run("INSERT INTO _migrations (id) VALUES (?)", [migration.id]);
249
+ }
250
+ }
251
+ }
252
+ // src/db/orgs.ts
253
+ function rowToOrg(row) {
254
+ return {
255
+ id: row.id,
256
+ name: row.name,
257
+ slug: row.slug,
258
+ description: row.description,
259
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
260
+ created_at: row.created_at,
261
+ updated_at: row.updated_at
262
+ };
263
+ }
264
+ function createOrg(input, db) {
265
+ db = db || getDatabase();
266
+ const id = crypto.randomUUID().slice(0, 8);
267
+ const slug = input.slug || input.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
268
+ try {
269
+ db.run(`INSERT INTO orgs (id, name, slug, description, metadata) VALUES (?, ?, ?, ?, ?)`, [id, input.name, slug, input.description || null, JSON.stringify(input.metadata || {})]);
270
+ } catch (e) {
271
+ if (e.message?.includes("UNIQUE constraint failed")) {
272
+ throw new ConflictError(`Org slug "${slug}" already exists`);
273
+ }
274
+ throw e;
275
+ }
276
+ return getOrg(id, db);
277
+ }
278
+ function getOrg(id, db) {
279
+ db = db || getDatabase();
280
+ const row = db.query("SELECT * FROM orgs WHERE id = ?").get(id);
281
+ return row ? rowToOrg(row) : null;
282
+ }
283
+ function getOrgBySlug(slug, db) {
284
+ db = db || getDatabase();
285
+ const row = db.query("SELECT * FROM orgs WHERE slug = ?").get(slug);
286
+ return row ? rowToOrg(row) : null;
287
+ }
288
+ function listOrgs(db) {
289
+ db = db || getDatabase();
290
+ const rows = db.query("SELECT * FROM orgs ORDER BY name").all();
291
+ return rows.map(rowToOrg);
292
+ }
293
+ function updateOrg(id, input, db) {
294
+ db = db || getDatabase();
295
+ const existing = getOrg(id, db);
296
+ if (!existing)
297
+ throw new NotFoundError("Org", id);
298
+ const name = input.name ?? existing.name;
299
+ const description = input.description !== undefined ? input.description : existing.description;
300
+ const metadata = input.metadata ? JSON.stringify(input.metadata) : existing.metadata ? JSON.stringify(existing.metadata) : "{}";
301
+ db.run(`UPDATE orgs SET name = ?, description = ?, metadata = ?, updated_at = datetime('now') WHERE id = ?`, [name, description, metadata, id]);
302
+ return getOrg(id, db);
303
+ }
304
+ function deleteOrg(id, db) {
305
+ db = db || getDatabase();
306
+ const result = db.run(`DELETE FROM orgs WHERE id = ?`, [id]);
307
+ return result.changes > 0;
308
+ }
309
+ // src/db/agents.ts
310
+ function rowToAgent(row) {
311
+ return {
312
+ id: row.id,
313
+ name: row.name,
314
+ description: row.description,
315
+ role: row.role,
316
+ title: row.title,
317
+ level: row.level,
318
+ capabilities: row.capabilities ? JSON.parse(row.capabilities) : [],
319
+ status: row.status,
320
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
321
+ created_at: row.created_at,
322
+ last_seen_at: row.last_seen_at,
323
+ session_id: row.session_id,
324
+ working_dir: row.working_dir,
325
+ active_org_id: row.active_org_id
326
+ };
327
+ }
328
+ function registerAgent(input, db) {
329
+ db = db || getDatabase();
330
+ const existing = db.query("SELECT * FROM agents WHERE name = ?").get(input.name);
331
+ if (existing) {
332
+ const row = existing;
333
+ if (input.force) {
334
+ db.run(`UPDATE agents SET last_seen_at = datetime('now'), session_id = ?, working_dir = ?, active_org_id = ?, description = ?, role = ?, title = ?, level = ?, capabilities = ? WHERE name = ?`, [input.session_id || null, input.working_dir || null, input.org_id || null, input.description || row.description, input.role || row.role, input.title || row.title, input.level || row.level, JSON.stringify(input.capabilities || JSON.parse(row.capabilities || "[]")), input.name]);
335
+ return getAgent(row.id, db);
336
+ }
337
+ throw new ConflictError(`Agent name "${input.name}" already exists`);
338
+ }
339
+ const id = crypto.randomUUID().slice(0, 8);
340
+ db.run(`INSERT INTO agents (id, name, description, role, title, level, capabilities, session_id, working_dir, active_org_id, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.name, input.description || null, input.role || null, input.title || null, input.level || null, JSON.stringify(input.capabilities || []), input.session_id || null, input.working_dir || null, input.org_id || null, JSON.stringify(input.metadata || {})]);
341
+ return getAgent(id, db);
342
+ }
343
+ function getAgent(id, db) {
344
+ db = db || getDatabase();
345
+ const row = db.query("SELECT * FROM agents WHERE id = ?").get(id);
346
+ return row ? rowToAgent(row) : null;
347
+ }
348
+ function getAgentByName(name, db) {
349
+ db = db || getDatabase();
350
+ const row = db.query("SELECT * FROM agents WHERE name = ?").get(name);
351
+ return row ? rowToAgent(row) : null;
352
+ }
353
+ function listAgents(db) {
354
+ db = db || getDatabase();
355
+ const rows = db.query("SELECT * FROM agents ORDER BY name").all();
356
+ return rows.map(rowToAgent);
357
+ }
358
+ function heartbeat(id, db) {
359
+ db = db || getDatabase();
360
+ db.run(`UPDATE agents SET last_seen_at = datetime('now') WHERE id = ?`, [id]);
361
+ return getAgent(id, db);
362
+ }
363
+ // src/db/calendars.ts
364
+ function rowToCalendar(row) {
365
+ return {
366
+ id: row.id,
367
+ org_id: row.org_id,
368
+ owner_id: row.owner_id,
369
+ slug: row.slug,
370
+ name: row.name,
371
+ description: row.description,
372
+ color: row.color,
373
+ timezone: row.timezone,
374
+ visibility: row.visibility,
375
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
376
+ created_at: row.created_at,
377
+ updated_at: row.updated_at
378
+ };
379
+ }
380
+ function createCalendar(input, db) {
381
+ db = db || getDatabase();
382
+ const id = crypto.randomUUID().slice(0, 8);
383
+ const slug = input.slug || input.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
384
+ try {
385
+ db.run(`INSERT INTO calendars (id, org_id, owner_id, slug, name, description, color, timezone, visibility, metadata)
386
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.org_id, input.owner_id || null, slug, input.name, input.description || null, input.color || null, input.timezone || "UTC", input.visibility || "org", JSON.stringify(input.metadata || {})]);
387
+ } catch (e) {
388
+ if (e.message?.includes("UNIQUE constraint failed")) {
389
+ throw new ConflictError(`Calendar slug "${slug}" already exists in this org`);
390
+ }
391
+ throw e;
392
+ }
393
+ return getCalendar(id, db);
394
+ }
395
+ function getCalendar(id, db) {
396
+ db = db || getDatabase();
397
+ const row = db.query("SELECT * FROM calendars WHERE id = ?").get(id);
398
+ return row ? rowToCalendar(row) : null;
399
+ }
400
+ function listCalendars(orgId, db) {
401
+ db = db || getDatabase();
402
+ let rows;
403
+ if (orgId) {
404
+ rows = db.query("SELECT * FROM calendars WHERE org_id = ? ORDER BY name").all(orgId);
405
+ } else {
406
+ rows = db.query("SELECT * FROM calendars ORDER BY org_id, name").all();
407
+ }
408
+ return rows.map(rowToCalendar);
409
+ }
410
+ function updateCalendar(id, input, db) {
411
+ db = db || getDatabase();
412
+ const existing = getCalendar(id, db);
413
+ if (!existing)
414
+ throw new NotFoundError("Calendar", id);
415
+ db.run(`UPDATE calendars SET name = ?, description = ?, color = ?, timezone = ?, visibility = ?, metadata = ?, updated_at = datetime('now') WHERE id = ?`, [input.name ?? existing.name, input.description !== undefined ? input.description : existing.description, input.color !== undefined ? input.color : existing.color, input.timezone ?? existing.timezone, input.visibility ?? existing.visibility, JSON.stringify(input.metadata ?? existing.metadata), id]);
416
+ return getCalendar(id, db);
417
+ }
418
+ function deleteCalendar(id, db) {
419
+ db = db || getDatabase();
420
+ const result = db.run(`DELETE FROM calendars WHERE id = ?`, [id]);
421
+ return result.changes > 0;
422
+ }
423
+ // src/db/events.ts
424
+ function rowToEvent(row) {
425
+ return {
426
+ id: row.id,
427
+ calendar_id: row.calendar_id,
428
+ org_id: row.org_id,
429
+ title: row.title,
430
+ description: row.description,
431
+ location: row.location,
432
+ start_at: row.start_at,
433
+ end_at: row.end_at,
434
+ all_day: !!row.all_day,
435
+ timezone: row.timezone,
436
+ status: row.status,
437
+ busy_type: row.busy_type,
438
+ visibility: row.visibility,
439
+ recurrence_rule: row.recurrence_rule,
440
+ recurrence_exception_dates: row.recurrence_exception_dates ? JSON.parse(row.recurrence_exception_dates) : null,
441
+ source_task_id: row.source_task_id,
442
+ created_by: row.created_by,
443
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
444
+ created_at: row.created_at,
445
+ updated_at: row.updated_at
446
+ };
447
+ }
448
+ function createEvent(input, db) {
449
+ db = db || getDatabase();
450
+ const id = crypto.randomUUID().slice(0, 8);
451
+ 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)
452
+ 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 || {})]);
453
+ return getEvent(id, db);
454
+ }
455
+ function getEvent(id, db) {
456
+ db = db || getDatabase();
457
+ const row = db.query("SELECT * FROM events WHERE id = ?").get(id);
458
+ return row ? rowToEvent(row) : null;
459
+ }
460
+ function listEvents(filter = {}, db) {
461
+ db = db || getDatabase();
462
+ const conditions = [];
463
+ const params = [];
464
+ if (filter.calendar_id) {
465
+ conditions.push("calendar_id = ?");
466
+ params.push(filter.calendar_id);
467
+ }
468
+ if (filter.org_id) {
469
+ conditions.push("org_id = ?");
470
+ params.push(filter.org_id);
471
+ }
472
+ if (filter.status) {
473
+ conditions.push("status = ?");
474
+ params.push(filter.status);
475
+ }
476
+ if (filter.after) {
477
+ conditions.push("start_at >= ?");
478
+ params.push(filter.after);
479
+ }
480
+ if (filter.before) {
481
+ conditions.push("start_at <= ?");
482
+ params.push(filter.before);
483
+ }
484
+ if (filter.created_by) {
485
+ conditions.push("created_by = ?");
486
+ params.push(filter.created_by);
487
+ }
488
+ if (filter.source_task_id) {
489
+ conditions.push("source_task_id = ?");
490
+ params.push(filter.source_task_id);
491
+ }
492
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
493
+ const limit = filter.limit ? `LIMIT ${filter.limit}` : "";
494
+ const offset = filter.offset ? `OFFSET ${filter.offset}` : "";
495
+ const rows = db.query(`SELECT * FROM events ${where} ORDER BY start_at ${limit} ${offset}`).all(...params);
496
+ return rows.map(rowToEvent);
497
+ }
498
+ function updateEvent(id, input, db) {
499
+ db = db || getDatabase();
500
+ const existing = getEvent(id, db);
501
+ if (!existing)
502
+ throw new NotFoundError("Event", id);
503
+ 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]);
504
+ return getEvent(id, db);
505
+ }
506
+ function deleteEvent(id, db) {
507
+ db = db || getDatabase();
508
+ const result = db.run(`DELETE FROM events WHERE id = ?`, [id]);
509
+ return result.changes > 0;
510
+ }
511
+ function findConflicts(calendarId, range, excludeEventId, db) {
512
+ db = db || getDatabase();
513
+ const exclude = excludeEventId ? "AND id != ?" : "";
514
+ const params = excludeEventId ? [calendarId, range.end, range.start, excludeEventId] : [calendarId, range.end, range.start];
515
+ 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);
516
+ return rows.map(rowToEvent);
517
+ }
518
+ function searchEvents(query, orgId, db) {
519
+ db = db || getDatabase();
520
+ const rows = db.query(`SELECT e.* FROM events e
521
+ INNER JOIN events_fts f ON f.rowid = e.rowid
522
+ WHERE events_fts MATCH ?
523
+ ${orgId ? "AND e.org_id = ?" : ""}
524
+ ORDER BY e.start_at`).all(query, ...orgId ? [orgId] : []);
525
+ return rows.map(rowToEvent);
526
+ }
527
+ // src/db/attendees.ts
528
+ function rowToAttendee(row) {
529
+ return {
530
+ id: row.id,
531
+ event_id: row.event_id,
532
+ agent_id: row.agent_id,
533
+ display_name: row.display_name,
534
+ email: row.email,
535
+ status: row.status,
536
+ required: !!row.required,
537
+ response_comment: row.response_comment,
538
+ responded_at: row.responded_at,
539
+ created_at: row.created_at
540
+ };
541
+ }
542
+ function createAttendee(input, db) {
543
+ db = db || getDatabase();
544
+ const id = crypto.randomUUID().slice(0, 8);
545
+ db.run(`INSERT INTO event_attendees (id, event_id, agent_id, display_name, email, status, required) VALUES (?, ?, ?, ?, ?, ?, ?)`, [id, input.event_id, input.agent_id || null, input.display_name || null, input.email || null, input.status || "needsAction", input.required !== undefined ? input.required ? 1 : 0 : 1]);
546
+ return getAttendee(id, db);
547
+ }
548
+ function getAttendee(id, db) {
549
+ db = db || getDatabase();
550
+ const row = db.query("SELECT * FROM event_attendees WHERE id = ?").get(id);
551
+ return row ? rowToAttendee(row) : null;
552
+ }
553
+ function getAttendeesForEvent(eventId, db) {
554
+ db = db || getDatabase();
555
+ const rows = db.query("SELECT * FROM event_attendees WHERE event_id = ? ORDER BY required DESC, created_at").all(eventId);
556
+ return rows.map(rowToAttendee);
557
+ }
558
+ function updateAttendee(id, input, db) {
559
+ db = db || getDatabase();
560
+ const existing = getAttendee(id, db);
561
+ if (!existing)
562
+ throw new NotFoundError("EventAttendee", id);
563
+ const newStatus = input.status ?? existing.status;
564
+ db.run(`UPDATE event_attendees SET status = ?, response_comment = ?, required = ?, responded_at = CASE WHEN ? IS NOT NULL AND responded_at IS NULL THEN datetime('now') ELSE responded_at END WHERE id = ?`, [newStatus, input.response_comment !== undefined ? input.response_comment : existing.response_comment, input.required !== undefined ? input.required ? 1 : 0 : existing.required ? 1 : 0, newStatus, id]);
565
+ return getAttendee(id, db);
566
+ }
567
+ // src/db/availability.ts
568
+ function rowToAvailability(row) {
569
+ return {
570
+ id: row.id,
571
+ agent_id: row.agent_id,
572
+ org_id: row.org_id,
573
+ day_of_week: row.day_of_week,
574
+ start_time: row.start_time,
575
+ end_time: row.end_time,
576
+ exceptions: row.exceptions ? JSON.parse(row.exceptions) : null,
577
+ created_at: row.created_at,
578
+ updated_at: row.updated_at
579
+ };
580
+ }
581
+ function createAvailability(input, db) {
582
+ db = db || getDatabase();
583
+ const id = crypto.randomUUID().slice(0, 8);
584
+ 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]);
585
+ return getAvailability(id, db);
586
+ }
587
+ function getAvailability(id, db) {
588
+ db = db || getDatabase();
589
+ const row = db.query("SELECT * FROM availability WHERE id = ?").get(id);
590
+ return row ? rowToAvailability(row) : null;
591
+ }
592
+ function getAvailabilityForAgent(agentId, orgId, db) {
593
+ db = db || getDatabase();
594
+ let rows;
595
+ if (orgId) {
596
+ rows = db.query("SELECT * FROM availability WHERE agent_id = ? AND org_id = ? ORDER BY day_of_week, start_time").all(agentId, orgId);
597
+ } else {
598
+ rows = db.query("SELECT * FROM availability WHERE agent_id = ? ORDER BY org_id, day_of_week, start_time").all(agentId);
599
+ }
600
+ return rows.map(rowToAvailability);
601
+ }
602
+ function upsertAgentAvailability(agentId, orgId, dayOfWeek, startTime, endTime, db) {
603
+ db = db || getDatabase();
604
+ const existing = db.query("SELECT * FROM availability WHERE agent_id = ? AND org_id = ? AND day_of_week = ?").all(agentId, orgId, dayOfWeek);
605
+ for (const row of existing) {
606
+ db.run(`DELETE FROM availability WHERE id = ?`, [row.id]);
607
+ }
608
+ return createAvailability({ agent_id: agentId, org_id: orgId, day_of_week: dayOfWeek, start_time: startTime, end_time: endTime }, db);
609
+ }
610
+ // src/db/memberships.ts
611
+ function rowToMembership(row) {
612
+ return {
613
+ id: row.id,
614
+ org_id: row.org_id,
615
+ agent_id: row.agent_id,
616
+ role: row.role,
617
+ created_at: row.created_at
618
+ };
619
+ }
620
+ function createMembership(input, db) {
621
+ db = db || getDatabase();
622
+ const id = crypto.randomUUID().slice(0, 8);
623
+ try {
624
+ db.run(`INSERT INTO org_memberships (id, org_id, agent_id, role) VALUES (?, ?, ?, ?)`, [id, input.org_id, input.agent_id, input.role || "member"]);
625
+ } catch (e) {
626
+ if (e.message?.includes("UNIQUE constraint failed")) {
627
+ throw new ConflictError(`Agent ${input.agent_id} is already a member of org ${input.org_id}`);
628
+ }
629
+ throw e;
630
+ }
631
+ return getMembership(id, db);
632
+ }
633
+ function getMembership(id, db) {
634
+ db = db || getDatabase();
635
+ const row = db.query("SELECT * FROM org_memberships WHERE id = ?").get(id);
636
+ return row ? rowToMembership(row) : null;
637
+ }
638
+ function getMembershipsForOrg(orgId, db) {
639
+ db = db || getDatabase();
640
+ const rows = db.query("SELECT * FROM org_memberships WHERE org_id = ? ORDER BY role DESC, created_at").all(orgId);
641
+ return rows.map(rowToMembership);
642
+ }
643
+ // src/server/serve.ts
644
+ function json(data, status = 200) {
645
+ return new Response(JSON.stringify(data, null, 2), {
646
+ status,
647
+ headers: { "Content-Type": "application/json" }
648
+ });
649
+ }
650
+ function error(message, status = 400) {
651
+ return json({ error: message }, status);
652
+ }
653
+ async function readBody(req) {
654
+ try {
655
+ return await req.json();
656
+ } catch {
657
+ return {};
658
+ }
659
+ }
660
+ function serve(port) {
661
+ const sseClients = new Set;
662
+ function broadcastEvent(event) {
663
+ const data = `data: ${JSON.stringify({ ...event, timestamp: new Date().toISOString() })}
664
+
665
+ `;
666
+ for (const client of sseClients) {
667
+ if (client.orgId && event.org_id !== client.orgId)
668
+ continue;
669
+ if (client.agentId && event.agent_id !== client.agentId)
670
+ continue;
671
+ try {
672
+ client.controller.enqueue(data);
673
+ } catch {}
674
+ }
675
+ }
676
+ const server = Bun.serve({
677
+ port,
678
+ async fetch(req) {
679
+ const url = new URL(req.url);
680
+ const path = url.pathname;
681
+ if (req.method === "OPTIONS") {
682
+ return new Response(null, {
683
+ headers: {
684
+ "Access-Control-Allow-Origin": "*",
685
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
686
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
687
+ }
688
+ });
689
+ }
690
+ if (path === "/api/events/stream") {
691
+ const client = {
692
+ controller: null,
693
+ orgId: url.searchParams.get("org_id") || undefined,
694
+ agentId: url.searchParams.get("agent_id") || undefined
695
+ };
696
+ const stream = new ReadableStream({
697
+ start(controller) {
698
+ client.controller = controller;
699
+ sseClients.add(client);
700
+ controller.enqueue(`data: ${JSON.stringify({ type: "connected", timestamp: new Date().toISOString() })}
701
+
702
+ `);
703
+ },
704
+ cancel() {
705
+ sseClients.delete(client);
706
+ }
707
+ });
708
+ return new Response(stream, {
709
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }
710
+ });
711
+ }
712
+ if (path === "/api/orgs" && req.method === "GET")
713
+ return json(listOrgs());
714
+ if (path === "/api/orgs" && req.method === "POST") {
715
+ const body = await readBody(req);
716
+ const org = createOrg(body);
717
+ broadcastEvent({ type: "org.created", action: "created" });
718
+ return json(org, 201);
719
+ }
720
+ if (path.startsWith("/api/orgs/") && req.method === "GET") {
721
+ const id = path.split("/").pop();
722
+ const org = getOrg(id) || getOrgBySlug(id);
723
+ return org ? json(org) : error("Org not found", 404);
724
+ }
725
+ if (path.startsWith("/api/orgs/") && req.method === "PUT") {
726
+ const id = path.split("/").pop();
727
+ const body = await readBody(req);
728
+ return json(updateOrg(id, body));
729
+ }
730
+ if (path.startsWith("/api/orgs/") && req.method === "DELETE") {
731
+ const id = path.split("/").pop();
732
+ return json({ deleted: deleteOrg(id) });
733
+ }
734
+ if (path === "/api/agents" && req.method === "GET")
735
+ return json(listAgents());
736
+ if (path === "/api/agents" && req.method === "POST") {
737
+ const body = await readBody(req);
738
+ return json(registerAgent(body), 201);
739
+ }
740
+ if (path.startsWith("/api/agents/") && path.endsWith("/heartbeat") && req.method === "POST") {
741
+ const id = path.split("/")[3];
742
+ if (!id)
743
+ return error("Agent ID required", 400);
744
+ heartbeat(id);
745
+ return json({ ok: true });
746
+ }
747
+ if (path.startsWith("/api/agents/") && req.method === "GET") {
748
+ const id = path.split("/").pop();
749
+ const agent = getAgent(id) || getAgentByName(id);
750
+ return agent ? json(agent) : error("Agent not found", 404);
751
+ }
752
+ if (path === "/api/calendars" && req.method === "GET") {
753
+ const orgId = url.searchParams.get("org_id") || undefined;
754
+ return json(listCalendars(orgId));
755
+ }
756
+ if (path === "/api/calendars" && req.method === "POST") {
757
+ const body = await readBody(req);
758
+ return json(createCalendar(body), 201);
759
+ }
760
+ if (path.startsWith("/api/calendars/") && req.method === "GET") {
761
+ const id = path.split("/").pop();
762
+ const cal = getCalendar(id);
763
+ return cal ? json(cal) : error("Calendar not found", 404);
764
+ }
765
+ if (path.startsWith("/api/calendars/") && req.method === "PUT") {
766
+ const id = path.split("/").pop();
767
+ const body = await readBody(req);
768
+ return json(updateCalendar(id, body));
769
+ }
770
+ if (path.startsWith("/api/calendars/") && req.method === "DELETE") {
771
+ const id = path.split("/").pop();
772
+ return json({ deleted: deleteCalendar(id) });
773
+ }
774
+ if (path === "/api/events" && req.method === "GET") {
775
+ return json(listEvents({
776
+ calendar_id: url.searchParams.get("calendar_id") || undefined,
777
+ org_id: url.searchParams.get("org_id") || undefined,
778
+ after: url.searchParams.get("after") || undefined,
779
+ before: url.searchParams.get("before") || undefined,
780
+ limit: url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit")) : undefined
781
+ }));
782
+ }
783
+ if (path === "/api/events" && req.method === "POST") {
784
+ const body = await readBody(req);
785
+ const evt = createEvent(body);
786
+ broadcastEvent({ type: "event.created", event_id: evt.id, action: "created", org_id: evt.org_id });
787
+ return json(evt, 201);
788
+ }
789
+ if (path === "/api/events/search" && req.method === "GET") {
790
+ const query = url.searchParams.get("q") || "";
791
+ const orgId = url.searchParams.get("org_id") || undefined;
792
+ return json(searchEvents(query, orgId));
793
+ }
794
+ if (path === "/api/events/conflicts" && req.method === "GET") {
795
+ const calendarId = url.searchParams.get("calendar_id") || "";
796
+ const start = url.searchParams.get("start") || "";
797
+ const end = url.searchParams.get("end") || "";
798
+ return json(findConflicts(calendarId, { start, end }));
799
+ }
800
+ if (path.startsWith("/api/events/") && req.method === "GET") {
801
+ const id = path.split("/").pop();
802
+ const evt = getEvent(id);
803
+ if (!evt)
804
+ return error("Event not found", 404);
805
+ const attendees2 = getAttendeesForEvent(id);
806
+ return json({ event: evt, attendees: attendees2 });
807
+ }
808
+ if (path.startsWith("/api/events/") && req.method === "PUT") {
809
+ const id = path.split("/").pop();
810
+ const body = await readBody(req);
811
+ return json(updateEvent(id, body));
812
+ }
813
+ if (path.startsWith("/api/events/") && req.method === "DELETE") {
814
+ const id = path.split("/").pop();
815
+ return json({ deleted: deleteEvent(id) });
816
+ }
817
+ if (path === "/api/attendees" && req.method === "POST") {
818
+ const body = await readBody(req);
819
+ return json(createAttendee(body), 201);
820
+ }
821
+ if (path.startsWith("/api/attendees/") && path.endsWith("/respond") && req.method === "POST") {
822
+ const id = path.split("/")[3];
823
+ if (!id)
824
+ return error("Attendee ID required", 400);
825
+ const body = await readBody(req);
826
+ return json(updateAttendee(id, body));
827
+ }
828
+ if (path === "/api/availability" && req.method === "GET") {
829
+ const agentId = url.searchParams.get("agent_id") || "";
830
+ const orgId = url.searchParams.get("org_id") || undefined;
831
+ return json(getAvailabilityForAgent(agentId, orgId));
832
+ }
833
+ if (path === "/api/availability" && req.method === "POST") {
834
+ const body = await readBody(req);
835
+ return json(upsertAgentAvailability(body.agent_id, body.org_id, body.day_of_week, body.start_time, body.end_time), 201);
836
+ }
837
+ if (path === "/api/members" && req.method === "GET") {
838
+ const orgId = url.searchParams.get("org_id") || "";
839
+ return json(getMembershipsForOrg(orgId));
840
+ }
841
+ if (path === "/api/members" && req.method === "POST") {
842
+ const body = await readBody(req);
843
+ return json(createMembership(body), 201);
844
+ }
845
+ if (path === "/api/health")
846
+ return json({ status: "ok", uptime: process.uptime() });
847
+ return new Response("Not found", { status: 404 });
848
+ }
849
+ });
850
+ console.log(`Calendar server listening on http://localhost:${port}`);
851
+ process.on("SIGINT", () => {
852
+ closeDatabase();
853
+ server.stop();
854
+ process.exit(0);
855
+ });
856
+ return server;
857
+ }
858
+
859
+ // src/server/index.ts
860
+ var port = parseInt(process.env["CALENDAR_PORT"] || "19428");
861
+ console.log(`Starting calendar server on port ${port}...`);
862
+ serve(port);