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