circal-mcp 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +76 -28
  2. package/dist/circal-mcp.mjs +1424 -701
  3. package/package.json +9 -7
@@ -456,9 +456,9 @@ var require_react_production = __commonJS({
456
456
  exports.useState = function(initialState) {
457
457
  return ReactSharedInternals.H.useState(initialState);
458
458
  };
459
- exports.useSyncExternalStore = function(subscribe, getSnapshot, getServerSnapshot) {
459
+ exports.useSyncExternalStore = function(subscribe2, getSnapshot, getServerSnapshot) {
460
460
  return ReactSharedInternals.H.useSyncExternalStore(
461
- subscribe,
461
+ subscribe2,
462
462
  getSnapshot,
463
463
  getServerSnapshot
464
464
  );
@@ -1426,9 +1426,9 @@ var require_react_development = __commonJS({
1426
1426
  exports.useState = function(initialState) {
1427
1427
  return resolveDispatcher().useState(initialState);
1428
1428
  };
1429
- exports.useSyncExternalStore = function(subscribe, getSnapshot, getServerSnapshot) {
1429
+ exports.useSyncExternalStore = function(subscribe2, getSnapshot, getServerSnapshot) {
1430
1430
  return resolveDispatcher().useSyncExternalStore(
1431
- subscribe,
1431
+ subscribe2,
1432
1432
  getSnapshot,
1433
1433
  getServerSnapshot
1434
1434
  );
@@ -1458,10 +1458,100 @@ var require_react = __commonJS({
1458
1458
  import { McpServer } from "@modelcontextprotocol/server";
1459
1459
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
1460
1460
 
1461
- // mcp/file.ts
1462
- import { randomBytes } from "node:crypto";
1463
- import { open, readFile, rename, rm } from "node:fs/promises";
1464
- import { dirname, join } from "node:path";
1461
+ // mcp/cloud.ts
1462
+ import { chmod, mkdir, readFile as readFile2, rm as rm2, writeFile } from "node:fs/promises";
1463
+ import { homedir } from "node:os";
1464
+ import { join as join2 } from "node:path";
1465
+ import { createInterface } from "node:readline";
1466
+ import { Writable } from "node:stream";
1467
+
1468
+ // src/lib/base64.ts
1469
+ function bytesToBase64Url(bytes) {
1470
+ let binary = "";
1471
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1472
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1473
+ }
1474
+ function base64UrlToBytes(value) {
1475
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) return null;
1476
+ try {
1477
+ const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
1478
+ const bytes = new Uint8Array(binary.length);
1479
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
1480
+ return bytes;
1481
+ } catch {
1482
+ return null;
1483
+ }
1484
+ }
1485
+
1486
+ // src/lib/cloudLink.ts
1487
+ var import_react3 = __toESM(require_react(), 1);
1488
+
1489
+ // src/lib/notify.ts
1490
+ var import_react = __toESM(require_react(), 1);
1491
+
1492
+ // src/lib/fuzzy.ts
1493
+ var MATCH = 10;
1494
+ var RUN = 9;
1495
+ var WORD_START = 12;
1496
+ var LEAD = 6;
1497
+ var GAP = 1;
1498
+ var GAP_CAP = 12;
1499
+ var LENGTH = 0.2;
1500
+ var TYPO = 40;
1501
+ var TYPO_MIN = 4;
1502
+ var WORD_BREAK = /[\s\-_/:.,(–—]/;
1503
+ function scan(text, query, start = 0) {
1504
+ let score = 0;
1505
+ let at = start;
1506
+ let previous = -2;
1507
+ let run = 0;
1508
+ for (const character of query) {
1509
+ if (character === " ") continue;
1510
+ const found = text.indexOf(character, at);
1511
+ if (found === -1) return null;
1512
+ score += MATCH;
1513
+ if (found === previous + 1) score += RUN * ++run;
1514
+ else {
1515
+ run = 0;
1516
+ score -= Math.min(found - at, GAP_CAP) * GAP;
1517
+ }
1518
+ if (found === 0) score += WORD_START + LEAD;
1519
+ else if (WORD_BREAK.test(text[found - 1])) score += WORD_START;
1520
+ previous = found;
1521
+ at = found + 1;
1522
+ }
1523
+ return score - text.length * LENGTH;
1524
+ }
1525
+ function bestScan(text, query) {
1526
+ const first = query.replace(/\s+/g, "")[0];
1527
+ if (!first) return null;
1528
+ let best = null;
1529
+ let tried = false;
1530
+ for (let i = text.indexOf(first); i !== -1; i = text.indexOf(first, i + 1)) {
1531
+ if (tried && i !== 0 && !WORD_BREAK.test(text[i - 1])) continue;
1532
+ tried = true;
1533
+ const score = scan(text, query, i);
1534
+ if (score !== null && (best === null || score > best)) best = score;
1535
+ }
1536
+ return best;
1537
+ }
1538
+ function fuzzyScore(text, query) {
1539
+ const wanted = query.trim().toLowerCase();
1540
+ if (!wanted) return 0;
1541
+ if (!text) return null;
1542
+ const haystack = text.toLowerCase();
1543
+ const clean = bestScan(haystack, wanted);
1544
+ if (clean !== null) return clean;
1545
+ const letters = wanted.replace(/\s+/g, "");
1546
+ if (letters.length < TYPO_MIN) return null;
1547
+ let best = null;
1548
+ for (let i = 0; i < wanted.length; i++) {
1549
+ if (wanted[i] === " ") continue;
1550
+ const score = bestScan(haystack, wanted.slice(0, i) + wanted.slice(i + 1));
1551
+ if (score !== null && (best === null || score > best)) best = score;
1552
+ }
1553
+ return best === null ? null : best - TYPO;
1554
+ }
1465
1555
 
1466
1556
  // src/lib/time.ts
1467
1557
  var MINUTES_PER_DAY = 1440;
@@ -2096,70 +2186,6 @@ function splitRule(rule, seedMs, at) {
2096
2186
  };
2097
2187
  }
2098
2188
 
2099
- // src/lib/fuzzy.ts
2100
- var MATCH = 10;
2101
- var RUN = 9;
2102
- var WORD_START = 12;
2103
- var LEAD = 6;
2104
- var GAP = 1;
2105
- var GAP_CAP = 12;
2106
- var LENGTH = 0.2;
2107
- var TYPO = 40;
2108
- var TYPO_MIN = 4;
2109
- var WORD_BREAK = /[\s\-_/:.,(–—]/;
2110
- function scan(text, query, start = 0) {
2111
- let score = 0;
2112
- let at = start;
2113
- let previous = -2;
2114
- let run = 0;
2115
- for (const character of query) {
2116
- if (character === " ") continue;
2117
- const found = text.indexOf(character, at);
2118
- if (found === -1) return null;
2119
- score += MATCH;
2120
- if (found === previous + 1) score += RUN * ++run;
2121
- else {
2122
- run = 0;
2123
- score -= Math.min(found - at, GAP_CAP) * GAP;
2124
- }
2125
- if (found === 0) score += WORD_START + LEAD;
2126
- else if (WORD_BREAK.test(text[found - 1])) score += WORD_START;
2127
- previous = found;
2128
- at = found + 1;
2129
- }
2130
- return score - text.length * LENGTH;
2131
- }
2132
- function bestScan(text, query) {
2133
- const first = query.replace(/\s+/g, "")[0];
2134
- if (!first) return null;
2135
- let best = null;
2136
- let tried = false;
2137
- for (let i = text.indexOf(first); i !== -1; i = text.indexOf(first, i + 1)) {
2138
- if (tried && i !== 0 && !WORD_BREAK.test(text[i - 1])) continue;
2139
- tried = true;
2140
- const score = scan(text, query, i);
2141
- if (score !== null && (best === null || score > best)) best = score;
2142
- }
2143
- return best;
2144
- }
2145
- function fuzzyScore(text, query) {
2146
- const wanted = query.trim().toLowerCase();
2147
- if (!wanted) return 0;
2148
- if (!text) return null;
2149
- const haystack = text.toLowerCase();
2150
- const clean = bestScan(haystack, wanted);
2151
- if (clean !== null) return clean;
2152
- const letters = wanted.replace(/\s+/g, "");
2153
- if (letters.length < TYPO_MIN) return null;
2154
- let best = null;
2155
- for (let i = 0; i < wanted.length; i++) {
2156
- if (wanted[i] === " ") continue;
2157
- const score = bestScan(haystack, wanted.slice(0, i) + wanted.slice(i + 1));
2158
- if (score !== null && (best === null || score > best)) best = score;
2159
- }
2160
- return best === null ? null : best - TYPO;
2161
- }
2162
-
2163
2189
  // src/lib/events.ts
2164
2190
  var ICON_MAX = 16;
2165
2191
  var ICON_NAME_MAX = 48;
@@ -2234,6 +2260,25 @@ function readGuests(list, count) {
2234
2260
  attendeeCount: Math.min(Math.max(claimed, people.length), ATTENDEE_COUNT_MAX)
2235
2261
  };
2236
2262
  }
2263
+ function readStamp(value) {
2264
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return 0;
2265
+ return Math.floor(value);
2266
+ }
2267
+ var GRAVE_TTL = 90 * 864e5;
2268
+ var GRAVE_MAX = 5e3;
2269
+ function readGraves(value) {
2270
+ if (!Array.isArray(value)) return [];
2271
+ const latest = /* @__PURE__ */ new Map();
2272
+ for (const entry of value) {
2273
+ if (entry === null || typeof entry !== "object") continue;
2274
+ const { id, at } = entry;
2275
+ if (typeof id !== "string" || !id) continue;
2276
+ const when = readStamp(at);
2277
+ if (when === 0) continue;
2278
+ latest.set(id, Math.max(latest.get(id) ?? 0, when));
2279
+ }
2280
+ return [...latest].map(([id, at]) => ({ id, at })).sort((a, b) => b.at - a.at).slice(0, GRAVE_MAX);
2281
+ }
2237
2282
  function overridesIn(events) {
2238
2283
  const out = /* @__PURE__ */ new Map();
2239
2284
  for (const event of events) {
@@ -2376,505 +2421,64 @@ function spanLabel(occ) {
2376
2421
  return occ.dayCount > 1 ? `Day ${occ.dayIndex}/${occ.dayCount}` : null;
2377
2422
  }
2378
2423
 
2379
- // src/lib/ics.ts
2380
- var encoder = new TextEncoder();
2381
- function fnv1a(source) {
2382
- let h = 2166136261;
2383
- for (let i = 0; i < source.length; i++) {
2384
- h ^= source.charCodeAt(i);
2385
- h = Math.imul(h, 16777619) >>> 0;
2386
- }
2387
- return h.toString(36);
2388
- }
2389
-
2390
- // src/lib/places.ts
2391
- var COORDS = {
2392
- "Africa/Abidjan": [5.32, -4.03],
2393
- "Africa/Accra": [5.55, -0.22],
2394
- "Africa/Addis_Ababa": [9.03, 38.7],
2395
- "Africa/Algiers": [36.78, 3.05],
2396
- "Africa/Asmara": [15.33, 38.88],
2397
- "Africa/Bamako": [12.65, -8],
2398
- "Africa/Bangui": [4.37, 18.58],
2399
- "Africa/Banjul": [13.47, -16.65],
2400
- "Africa/Bissau": [11.85, -15.58],
2401
- "Africa/Blantyre": [-15.78, 35],
2402
- "Africa/Brazzaville": [-4.27, 15.28],
2403
- "Africa/Bujumbura": [-3.38, 29.37],
2404
- "Africa/Cairo": [30.05, 31.25],
2405
- "Africa/Casablanca": [33.65, -7.58],
2406
- "Africa/Ceuta": [35.88, -5.32],
2407
- "Africa/Conakry": [9.52, -13.72],
2408
- "Africa/Dakar": [14.67, -17.43],
2409
- "Africa/Dar_es_Salaam": [-6.8, 39.28],
2410
- "Africa/Djibouti": [11.6, 43.15],
2411
- "Africa/Douala": [4.05, 9.7],
2412
- "Africa/El_Aaiun": [27.15, -13.2],
2413
- "Africa/Freetown": [8.5, -13.25],
2414
- "Africa/Gaborone": [-24.65, 25.92],
2415
- "Africa/Harare": [-17.83, 31.05],
2416
- "Africa/Johannesburg": [-26.25, 28],
2417
- "Africa/Juba": [4.85, 31.62],
2418
- "Africa/Kampala": [0.32, 32.42],
2419
- "Africa/Khartoum": [15.6, 32.53],
2420
- "Africa/Kigali": [-1.95, 30.07],
2421
- "Africa/Kinshasa": [-4.3, 15.3],
2422
- "Africa/Lagos": [6.45, 3.4],
2423
- "Africa/Libreville": [0.38, 9.45],
2424
- "Africa/Lome": [6.13, 1.22],
2425
- "Africa/Luanda": [-8.8, 13.23],
2426
- "Africa/Lubumbashi": [-11.67, 27.47],
2427
- "Africa/Lusaka": [-15.42, 28.28],
2428
- "Africa/Malabo": [3.75, 8.78],
2429
- "Africa/Maputo": [-25.97, 32.58],
2430
- "Africa/Maseru": [-29.47, 27.5],
2431
- "Africa/Mbabane": [-26.3, 31.1],
2432
- "Africa/Mogadishu": [2.07, 45.37],
2433
- "Africa/Monrovia": [6.3, -10.78],
2434
- "Africa/Nairobi": [-1.28, 36.82],
2435
- "Africa/Ndjamena": [12.12, 15.05],
2436
- "Africa/Niamey": [13.52, 2.12],
2437
- "Africa/Nouakchott": [18.1, -15.95],
2438
- "Africa/Ouagadougou": [12.37, -1.52],
2439
- "Africa/Porto-Novo": [6.48, 2.62],
2440
- "Africa/Sao_Tome": [0.33, 6.73],
2441
- "Africa/Tripoli": [32.9, 13.18],
2442
- "Africa/Tunis": [36.8, 10.18],
2443
- "Africa/Windhoek": [-22.57, 17.1],
2444
- "America/Adak": [51.88, -176.66],
2445
- "America/Anchorage": [61.22, -149.9],
2446
- "America/Anguilla": [18.2, -63.07],
2447
- "America/Antigua": [17.05, -61.8],
2448
- "America/Araguaina": [-7.2, -48.2],
2449
- "America/Argentina/Buenos_Aires": [-34.6, -58.45],
2450
- "America/Argentina/Catamarca": [-28.47, -65.78],
2451
- "America/Argentina/Cordoba": [-31.4, -64.18],
2452
- "America/Argentina/Jujuy": [-24.18, -65.3],
2453
- "America/Argentina/La_Rioja": [-29.43, -66.85],
2454
- "America/Argentina/Mendoza": [-32.88, -68.82],
2455
- "America/Argentina/Rio_Gallegos": [-51.63, -69.22],
2456
- "America/Argentina/Salta": [-24.78, -65.42],
2457
- "America/Argentina/San_Juan": [-31.53, -68.52],
2458
- "America/Argentina/San_Luis": [-33.32, -66.35],
2459
- "America/Argentina/Tucuman": [-26.82, -65.22],
2460
- "America/Argentina/Ushuaia": [-54.8, -68.3],
2461
- "America/Aruba": [12.5, -69.97],
2462
- "America/Asuncion": [-25.27, -57.67],
2463
- "America/Atikokan": [48.76, -91.62],
2464
- "America/Bahia": [-12.98, -38.52],
2465
- "America/Bahia_Banderas": [20.8, -105.25],
2466
- "America/Barbados": [13.1, -59.62],
2467
- "America/Belem": [-1.45, -48.48],
2468
- "America/Belize": [17.5, -88.2],
2469
- "America/Blanc-Sablon": [51.42, -57.12],
2470
- "America/Boa_Vista": [2.82, -60.67],
2471
- "America/Bogota": [4.6, -74.08],
2472
- "America/Boise": [43.61, -116.2],
2473
- "America/Cambridge_Bay": [69.11, -105.05],
2474
- "America/Campo_Grande": [-20.45, -54.62],
2475
- "America/Cancun": [21.08, -86.77],
2476
- "America/Caracas": [10.5, -66.93],
2477
- "America/Cayenne": [4.93, -52.33],
2478
- "America/Cayman": [19.3, -81.38],
2479
- "America/Chicago": [41.85, -87.65],
2480
- "America/Chihuahua": [28.63, -106.08],
2481
- "America/Ciudad_Juarez": [31.73, -106.48],
2482
- "America/Costa_Rica": [9.93, -84.08],
2483
- "America/Coyhaique": [-45.57, -72.07],
2484
- "America/Creston": [49.1, -116.52],
2485
- "America/Cuiaba": [-15.58, -56.08],
2486
- "America/Curacao": [12.18, -69],
2487
- "America/Danmarkshavn": [76.77, -18.67],
2488
- "America/Dawson": [64.07, -139.42],
2489
- "America/Dawson_Creek": [55.77, -120.23],
2490
- "America/Denver": [39.74, -104.98],
2491
- "America/Detroit": [42.33, -83.05],
2492
- "America/Dominica": [15.3, -61.4],
2493
- "America/Edmonton": [53.55, -113.47],
2494
- "America/Eirunepe": [-6.67, -69.87],
2495
- "America/El_Salvador": [13.7, -89.2],
2496
- "America/Fort_Nelson": [58.8, -122.7],
2497
- "America/Fortaleza": [-3.72, -38.5],
2498
- "America/Glace_Bay": [46.2, -59.95],
2499
- "America/Goose_Bay": [53.33, -60.42],
2500
- "America/Grand_Turk": [21.47, -71.13],
2501
- "America/Grenada": [12.05, -61.75],
2502
- "America/Guadeloupe": [16.23, -61.53],
2503
- "America/Guatemala": [14.63, -90.52],
2504
- "America/Guayaquil": [-2.17, -79.83],
2505
- "America/Guyana": [6.8, -58.17],
2506
- "America/Halifax": [44.65, -63.6],
2507
- "America/Havana": [23.13, -82.37],
2508
- "America/Hermosillo": [29.07, -110.97],
2509
- "America/Indiana/Indianapolis": [39.77, -86.16],
2510
- "America/Indiana/Knox": [41.3, -86.62],
2511
- "America/Indiana/Marengo": [38.38, -86.34],
2512
- "America/Indiana/Petersburg": [38.49, -87.28],
2513
- "America/Indiana/Tell_City": [37.95, -86.76],
2514
- "America/Indiana/Vevay": [38.75, -85.07],
2515
- "America/Indiana/Vincennes": [38.68, -87.53],
2516
- "America/Indiana/Winamac": [41.05, -86.6],
2517
- "America/Inuvik": [68.35, -133.72],
2518
- "America/Iqaluit": [63.73, -68.47],
2519
- "America/Jamaica": [17.97, -76.79],
2520
- "America/Juneau": [58.3, -134.42],
2521
- "America/Kentucky/Louisville": [38.25, -85.76],
2522
- "America/Kentucky/Monticello": [36.83, -84.85],
2523
- "America/Kralendijk": [12.15, -68.28],
2524
- "America/La_Paz": [-16.5, -68.15],
2525
- "America/Lima": [-12.05, -77.05],
2526
- "America/Los_Angeles": [34.05, -118.24],
2527
- "America/Lower_Princes": [18.05, -63.05],
2528
- "America/Maceio": [-9.67, -35.72],
2529
- "America/Managua": [12.15, -86.28],
2530
- "America/Manaus": [-3.13, -60.02],
2531
- "America/Marigot": [18.07, -63.08],
2532
- "America/Martinique": [14.6, -61.08],
2533
- "America/Matamoros": [25.83, -97.5],
2534
- "America/Mazatlan": [23.22, -106.42],
2535
- "America/Menominee": [45.11, -87.61],
2536
- "America/Merida": [20.97, -89.62],
2537
- "America/Metlakatla": [55.13, -131.58],
2538
- "America/Mexico_City": [19.4, -99.15],
2539
- "America/Miquelon": [47.05, -56.33],
2540
- "America/Moncton": [46.1, -64.78],
2541
- "America/Monterrey": [25.67, -100.32],
2542
- "America/Montevideo": [-34.91, -56.21],
2543
- "America/Montserrat": [16.72, -62.22],
2544
- "America/Nassau": [25.08, -77.35],
2545
- "America/New_York": [40.71, -74.01],
2546
- "America/Nome": [64.5, -165.41],
2547
- "America/Noronha": [-3.85, -32.42],
2548
- "America/North_Dakota/Beulah": [47.26, -101.78],
2549
- "America/North_Dakota/Center": [47.12, -101.3],
2550
- "America/North_Dakota/New_Salem": [46.84, -101.41],
2551
- "America/Nuuk": [64.18, -51.73],
2552
- "America/Ojinaga": [29.57, -104.42],
2553
- "America/Panama": [8.97, -79.53],
2554
- "America/Paramaribo": [5.83, -55.17],
2555
- "America/Phoenix": [33.45, -112.07],
2556
- "America/Port-au-Prince": [18.53, -72.33],
2557
- "America/Port_of_Spain": [10.65, -61.52],
2558
- "America/Porto_Velho": [-8.77, -63.9],
2559
- "America/Puerto_Rico": [18.47, -66.11],
2560
- "America/Punta_Arenas": [-53.15, -70.92],
2561
- "America/Rankin_Inlet": [62.82, -92.08],
2562
- "America/Recife": [-8.05, -34.9],
2563
- "America/Regina": [50.4, -104.65],
2564
- "America/Resolute": [74.7, -94.83],
2565
- "America/Rio_Branco": [-9.97, -67.8],
2566
- "America/Santarem": [-2.43, -54.87],
2567
- "America/Santiago": [-33.45, -70.67],
2568
- "America/Santo_Domingo": [18.47, -69.9],
2569
- "America/Sao_Paulo": [-23.53, -46.62],
2570
- "America/Scoresbysund": [70.48, -21.97],
2571
- "America/Sitka": [57.18, -135.3],
2572
- "America/St_Barthelemy": [17.88, -62.85],
2573
- "America/St_Johns": [47.57, -52.72],
2574
- "America/St_Kitts": [17.3, -62.72],
2575
- "America/St_Lucia": [14.02, -61],
2576
- "America/St_Thomas": [18.35, -64.93],
2577
- "America/St_Vincent": [13.15, -61.23],
2578
- "America/Swift_Current": [50.28, -107.83],
2579
- "America/Tegucigalpa": [14.1, -87.22],
2580
- "America/Thule": [76.57, -68.78],
2581
- "America/Tijuana": [32.53, -117.02],
2582
- "America/Toronto": [43.65, -79.38],
2583
- "America/Tortola": [18.45, -64.62],
2584
- "America/Vancouver": [49.27, -123.12],
2585
- "America/Whitehorse": [60.72, -135.05],
2586
- "America/Winnipeg": [49.88, -97.15],
2587
- "America/Yakutat": [59.55, -139.73],
2588
- "Antarctica/Casey": [-66.28, 110.52],
2589
- "Antarctica/Davis": [-68.58, 77.97],
2590
- "Antarctica/DumontDUrville": [-66.67, 140.02],
2591
- "Antarctica/Macquarie": [-54.5, 158.95],
2592
- "Antarctica/Mawson": [-67.6, 62.88],
2593
- "Antarctica/McMurdo": [-77.83, 166.6],
2594
- "Antarctica/Palmer": [-64.8, -64.1],
2595
- "Antarctica/Rothera": [-67.57, -68.13],
2596
- "Antarctica/Syowa": [-69.01, 39.59],
2597
- "Antarctica/Troll": [-72.01, 2.53],
2598
- "Antarctica/Vostok": [-78.4, 106.9],
2599
- "Arctic/Longyearbyen": [78, 16],
2600
- "Asia/Aden": [12.75, 45.2],
2601
- "Asia/Almaty": [43.25, 76.95],
2602
- "Asia/Amman": [31.95, 35.93],
2603
- "Asia/Anadyr": [64.75, 177.48],
2604
- "Asia/Aqtau": [44.52, 50.27],
2605
- "Asia/Aqtobe": [50.28, 57.17],
2606
- "Asia/Ashgabat": [37.95, 58.38],
2607
- "Asia/Atyrau": [47.12, 51.93],
2608
- "Asia/Baghdad": [33.35, 44.42],
2609
- "Asia/Bahrain": [26.38, 50.58],
2610
- "Asia/Baku": [40.38, 49.85],
2611
- "Asia/Bangkok": [13.75, 100.52],
2612
- "Asia/Barnaul": [53.37, 83.75],
2613
- "Asia/Beirut": [33.88, 35.5],
2614
- "Asia/Bishkek": [42.9, 74.6],
2615
- "Asia/Brunei": [4.93, 114.92],
2616
- "Asia/Chita": [52.05, 113.47],
2617
- "Asia/Colombo": [6.93, 79.85],
2618
- "Asia/Damascus": [33.5, 36.3],
2619
- "Asia/Dhaka": [23.72, 90.42],
2620
- "Asia/Dili": [-8.55, 125.58],
2621
- "Asia/Dubai": [25.3, 55.3],
2622
- "Asia/Dushanbe": [38.58, 68.8],
2623
- "Asia/Famagusta": [35.12, 33.95],
2624
- "Asia/Gaza": [31.5, 34.47],
2625
- "Asia/Hebron": [31.53, 35.09],
2626
- "Asia/Ho_Chi_Minh": [10.75, 106.67],
2627
- "Asia/Hong_Kong": [22.28, 114.15],
2628
- "Asia/Hovd": [48.02, 91.65],
2629
- "Asia/Irkutsk": [52.27, 104.33],
2630
- "Asia/Jakarta": [-6.17, 106.8],
2631
- "Asia/Jayapura": [-2.53, 140.7],
2632
- "Asia/Jerusalem": [31.78, 35.22],
2633
- "Asia/Kabul": [34.52, 69.2],
2634
- "Asia/Kamchatka": [53.02, 158.65],
2635
- "Asia/Karachi": [24.87, 67.05],
2636
- "Asia/Kathmandu": [27.72, 85.32],
2637
- "Asia/Khandyga": [62.66, 135.55],
2638
- "Asia/Kolkata": [22.53, 88.37],
2639
- "Asia/Krasnoyarsk": [56.02, 92.83],
2640
- "Asia/Kuala_Lumpur": [3.17, 101.7],
2641
- "Asia/Kuching": [1.55, 110.33],
2642
- "Asia/Kuwait": [29.33, 47.98],
2643
- "Asia/Macau": [22.2, 113.54],
2644
- "Asia/Magadan": [59.57, 150.8],
2645
- "Asia/Makassar": [-5.12, 119.4],
2646
- "Asia/Manila": [14.59, 120.97],
2647
- "Asia/Muscat": [23.6, 58.58],
2648
- "Asia/Nicosia": [35.17, 33.37],
2649
- "Asia/Novokuznetsk": [53.75, 87.12],
2650
- "Asia/Novosibirsk": [55.03, 82.92],
2651
- "Asia/Omsk": [55, 73.4],
2652
- "Asia/Oral": [51.22, 51.35],
2653
- "Asia/Phnom_Penh": [11.55, 104.92],
2654
- "Asia/Pontianak": [-0.03, 109.33],
2655
- "Asia/Pyongyang": [39.02, 125.75],
2656
- "Asia/Qatar": [25.28, 51.53],
2657
- "Asia/Qostanay": [53.2, 63.62],
2658
- "Asia/Qyzylorda": [44.8, 65.47],
2659
- "Asia/Riyadh": [24.63, 46.72],
2660
- "Asia/Sakhalin": [46.97, 142.7],
2661
- "Asia/Samarkand": [39.67, 66.8],
2662
- "Asia/Seoul": [37.55, 126.97],
2663
- "Asia/Shanghai": [31.23, 121.47],
2664
- "Asia/Singapore": [1.28, 103.85],
2665
- "Asia/Srednekolymsk": [67.47, 153.72],
2666
- "Asia/Taipei": [25.05, 121.5],
2667
- "Asia/Tashkent": [41.33, 69.3],
2668
- "Asia/Tbilisi": [41.72, 44.82],
2669
- "Asia/Tehran": [35.67, 51.43],
2670
- "Asia/Thimphu": [27.47, 89.65],
2671
- "Asia/Tokyo": [35.65, 139.74],
2672
- "Asia/Tomsk": [56.5, 84.97],
2673
- "Asia/Ulaanbaatar": [47.92, 106.88],
2674
- "Asia/Urumqi": [43.8, 87.58],
2675
- "Asia/Ust-Nera": [64.56, 143.23],
2676
- "Asia/Vientiane": [17.97, 102.6],
2677
- "Asia/Vladivostok": [43.17, 131.93],
2678
- "Asia/Yakutsk": [62, 129.67],
2679
- "Asia/Yangon": [16.78, 96.17],
2680
- "Asia/Yekaterinburg": [56.85, 60.6],
2681
- "Asia/Yerevan": [40.18, 44.5],
2682
- "Atlantic/Azores": [37.73, -25.67],
2683
- "Atlantic/Bermuda": [32.28, -64.77],
2684
- "Atlantic/Canary": [28.1, -15.4],
2685
- "Atlantic/Cape_Verde": [14.92, -23.52],
2686
- "Atlantic/Faroe": [62.02, -6.77],
2687
- "Atlantic/Madeira": [32.63, -16.9],
2688
- "Atlantic/Reykjavik": [64.15, -21.85],
2689
- "Atlantic/South_Georgia": [-54.27, -36.53],
2690
- "Atlantic/St_Helena": [-15.92, -5.7],
2691
- "Atlantic/Stanley": [-51.7, -57.85],
2692
- "Australia/Adelaide": [-34.92, 138.58],
2693
- "Australia/Brisbane": [-27.47, 153.03],
2694
- "Australia/Broken_Hill": [-31.95, 141.45],
2695
- "Australia/Darwin": [-12.47, 130.83],
2696
- "Australia/Eucla": [-31.72, 128.87],
2697
- "Australia/Hobart": [-42.88, 147.32],
2698
- "Australia/Lindeman": [-20.27, 149],
2699
- "Australia/Lord_Howe": [-31.55, 159.08],
2700
- "Australia/Melbourne": [-37.82, 144.97],
2701
- "Australia/Perth": [-31.95, 115.85],
2702
- "Australia/Sydney": [-33.87, 151.22],
2703
- "Europe/Amsterdam": [52.37, 4.9],
2704
- "Europe/Andorra": [42.5, 1.52],
2705
- "Europe/Astrakhan": [46.35, 48.05],
2706
- "Europe/Athens": [37.97, 23.72],
2707
- "Europe/Belgrade": [44.83, 20.5],
2708
- "Europe/Berlin": [52.5, 13.37],
2709
- "Europe/Bratislava": [48.15, 17.12],
2710
- "Europe/Brussels": [50.83, 4.33],
2711
- "Europe/Bucharest": [44.43, 26.1],
2712
- "Europe/Budapest": [47.5, 19.08],
2713
- "Europe/Busingen": [47.7, 8.68],
2714
- "Europe/Chisinau": [47, 28.83],
2715
- "Europe/Copenhagen": [55.67, 12.58],
2716
- "Europe/Dublin": [53.33, -6.25],
2717
- "Europe/Gibraltar": [36.13, -5.35],
2718
- "Europe/Guernsey": [49.45, -2.54],
2719
- "Europe/Helsinki": [60.17, 24.97],
2720
- "Europe/Isle_of_Man": [54.15, -4.47],
2721
- "Europe/Istanbul": [41.02, 28.97],
2722
- "Europe/Jersey": [49.18, -2.11],
2723
- "Europe/Kaliningrad": [54.72, 20.5],
2724
- "Europe/Kirov": [58.6, 49.65],
2725
- "Europe/Kyiv": [50.43, 30.52],
2726
- "Europe/Lisbon": [38.72, -9.13],
2727
- "Europe/Ljubljana": [46.05, 14.52],
2728
- "Europe/London": [51.51, -0.13],
2729
- "Europe/Luxembourg": [49.6, 6.15],
2730
- "Europe/Madrid": [40.4, -3.68],
2731
- "Europe/Malta": [35.9, 14.52],
2732
- "Europe/Mariehamn": [60.1, 19.95],
2733
- "Europe/Minsk": [53.9, 27.57],
2734
- "Europe/Monaco": [43.7, 7.38],
2735
- "Europe/Moscow": [55.76, 37.62],
2736
- "Europe/Oslo": [59.92, 10.75],
2737
- "Europe/Paris": [48.87, 2.33],
2738
- "Europe/Podgorica": [42.43, 19.27],
2739
- "Europe/Prague": [50.08, 14.43],
2740
- "Europe/Riga": [56.95, 24.1],
2741
- "Europe/Rome": [41.9, 12.48],
2742
- "Europe/Samara": [53.2, 50.15],
2743
- "Europe/San_Marino": [43.92, 12.47],
2744
- "Europe/Sarajevo": [43.87, 18.42],
2745
- "Europe/Saratov": [51.57, 46.03],
2746
- "Europe/Simferopol": [44.95, 34.1],
2747
- "Europe/Skopje": [41.98, 21.43],
2748
- "Europe/Sofia": [42.68, 23.32],
2749
- "Europe/Stockholm": [59.33, 18.05],
2750
- "Europe/Tallinn": [59.42, 24.75],
2751
- "Europe/Tirane": [41.33, 19.83],
2752
- "Europe/Ulyanovsk": [54.33, 48.4],
2753
- "Europe/Vaduz": [47.15, 9.52],
2754
- "Europe/Vatican": [41.9, 12.45],
2755
- "Europe/Vienna": [48.22, 16.33],
2756
- "Europe/Vilnius": [54.68, 25.32],
2757
- "Europe/Volgograd": [48.73, 44.42],
2758
- "Europe/Warsaw": [52.25, 21],
2759
- "Europe/Zagreb": [45.8, 15.97],
2760
- "Europe/Zurich": [47.38, 8.53],
2761
- "Indian/Antananarivo": [-18.92, 47.52],
2762
- "Indian/Chagos": [-7.33, 72.42],
2763
- "Indian/Christmas": [-10.42, 105.72],
2764
- "Indian/Cocos": [-12.17, 96.92],
2765
- "Indian/Comoro": [-11.68, 43.27],
2766
- "Indian/Kerguelen": [-49.35, 70.22],
2767
- "Indian/Mahe": [-4.67, 55.47],
2768
- "Indian/Maldives": [4.17, 73.5],
2769
- "Indian/Mauritius": [-20.17, 57.5],
2770
- "Indian/Mayotte": [-12.78, 45.23],
2771
- "Indian/Reunion": [-20.87, 55.47],
2772
- "Pacific/Apia": [-13.83, -171.73],
2773
- "Pacific/Auckland": [-36.87, 174.77],
2774
- "Pacific/Bougainville": [-6.22, 155.57],
2775
- "Pacific/Chatham": [-43.95, -176.55],
2776
- "Pacific/Chuuk": [7.42, 151.78],
2777
- "Pacific/Easter": [-27.15, -109.43],
2778
- "Pacific/Efate": [-17.67, 168.42],
2779
- "Pacific/Fakaofo": [-9.37, -171.23],
2780
- "Pacific/Fiji": [-18.13, 178.42],
2781
- "Pacific/Funafuti": [-8.52, 179.22],
2782
- "Pacific/Galapagos": [-0.9, -89.6],
2783
- "Pacific/Gambier": [-23.13, -134.95],
2784
- "Pacific/Guadalcanal": [-9.53, 160.2],
2785
- "Pacific/Guam": [13.47, 144.75],
2786
- "Pacific/Honolulu": [21.31, -157.86],
2787
- "Pacific/Kanton": [-2.78, -171.72],
2788
- "Pacific/Kiritimati": [1.87, -157.33],
2789
- "Pacific/Kosrae": [5.32, 162.98],
2790
- "Pacific/Kwajalein": [9.08, 167.33],
2791
- "Pacific/Majuro": [7.15, 171.2],
2792
- "Pacific/Marquesas": [-9, -139.5],
2793
- "Pacific/Midway": [28.22, -177.37],
2794
- "Pacific/Nauru": [-0.52, 166.92],
2795
- "Pacific/Niue": [-19.02, -169.92],
2796
- "Pacific/Norfolk": [-29.05, 167.97],
2797
- "Pacific/Noumea": [-22.27, 166.45],
2798
- "Pacific/Pago_Pago": [-14.27, -170.7],
2799
- "Pacific/Palau": [7.33, 134.48],
2800
- "Pacific/Pitcairn": [-25.07, -130.08],
2801
- "Pacific/Pohnpei": [6.97, 158.22],
2802
- "Pacific/Port_Moresby": [-9.5, 147.17],
2803
- "Pacific/Rarotonga": [-21.23, -159.77],
2804
- "Pacific/Saipan": [15.2, 145.75],
2805
- "Pacific/Tahiti": [-17.53, -149.57],
2806
- "Pacific/Tarawa": [1.42, 173],
2807
- "Pacific/Tongatapu": [-21.13, -175.2],
2808
- "Pacific/Wake": [19.28, 166.62],
2809
- "Pacific/Wallis": [-13.3, -176.17]
2810
- };
2811
- var ALIASES = {
2812
- "Africa/Asmera": "Africa/Asmara",
2813
- "America/Buenos_Aires": "America/Argentina/Buenos_Aires",
2814
- "America/Catamarca": "America/Argentina/Catamarca",
2815
- "America/Coral_Harbour": "America/Atikokan",
2816
- "America/Cordoba": "America/Argentina/Cordoba",
2817
- "America/Godthab": "America/Nuuk",
2818
- "America/Indianapolis": "America/Indiana/Indianapolis",
2819
- "America/Jujuy": "America/Argentina/Jujuy",
2820
- "America/Louisville": "America/Kentucky/Louisville",
2821
- "America/Mendoza": "America/Argentina/Mendoza",
2822
- "Asia/Calcutta": "Asia/Kolkata",
2823
- "Asia/Katmandu": "Asia/Kathmandu",
2824
- "Asia/Rangoon": "Asia/Yangon",
2825
- "Asia/Saigon": "Asia/Ho_Chi_Minh",
2826
- "Atlantic/Faeroe": "Atlantic/Faroe",
2827
- "Europe/Kiev": "Europe/Kyiv",
2828
- "Pacific/Enderbury": "Pacific/Kanton",
2829
- "Pacific/Ponape": "Pacific/Pohnpei",
2830
- "Pacific/Truk": "Pacific/Chuuk"
2831
- };
2832
- function placeForZone(zone) {
2833
- const coords = COORDS[zone] ?? COORDS[ALIASES[zone]];
2834
- if (!coords) return null;
2835
- return { zone, lat: coords[0], lon: coords[1], label: zoneLabel(zone) };
2836
- }
2837
- var PLACE_OPTIONS = Object.keys(COORDS).map((zone) => ({ value: zone, label: zoneLabel(zone) })).sort((a, b) => a.label.localeCompare(b.label));
2838
-
2839
- // src/lib/store.ts
2840
- var import_react2 = __toESM(require_react(), 1);
2841
-
2842
- // src/lib/notify.ts
2843
- var import_react = __toESM(require_react(), 1);
2844
-
2845
- // src/lib/mutate.ts
2846
- function applyUpdate(events, id, patch) {
2847
- return events.map((e) => e.id === id ? { ...e, ...patch } : e);
2848
- }
2849
- function applyToggleDone(events, id, at) {
2850
- return events.map((e) => e.id === id ? { ...e, doneKeys: toggleDoneKey(e.doneKeys, at) } : e);
2851
- }
2852
- function applyRemove(events, id) {
2853
- return events.filter((e) => e.id !== id && e.seriesId !== id);
2854
- }
2855
- function applySplit(events, id, at, tail, mintId) {
2856
- const original = events.find((e) => e.id === id);
2857
- if (!original) return { events, created: null };
2858
- const split = splitRule(original, original.start, at);
2859
- const created = { ...tail, ...split.tail, seriesId: "", seriesKey: "", id: mintId() };
2860
- const seed = new Date(original.start);
2861
- const detached = (e) => e.seriesId === id && (keyToInstant(e.seriesKey, seed) ?? -Infinity) >= at;
2862
- return {
2863
- events: [
2864
- ...events.map(
2865
- (e) => e.id === id ? { ...e, ...split.head } : detached(e) ? { ...e, seriesId: created.id } : e
2866
- ),
2867
- created
2868
- ],
2869
- created
2870
- };
2871
- }
2872
- function applyTruncate(events, id, at) {
2873
- const original = events.find((e) => e.id === id);
2874
- if (!original) return events;
2875
- const split = splitRule(original, original.start, at);
2876
- const seed = new Date(original.start);
2877
- return events.filter((e) => !(e.seriesId === id && (keyToInstant(e.seriesKey, seed) ?? -Infinity) >= at)).map((e) => e.id === id ? { ...e, ...split.head } : e);
2424
+ // src/lib/store.ts
2425
+ var import_react2 = __toESM(require_react(), 1);
2426
+
2427
+ // src/lib/mutate.ts
2428
+ function applyUpdate(events, id, patch, now) {
2429
+ return events.map((e) => e.id === id ? { ...e, ...patch, updatedAt: now } : e);
2430
+ }
2431
+ function applyToggleDone(events, id, at, now) {
2432
+ return events.map(
2433
+ (e) => e.id === id ? { ...e, doneKeys: toggleDoneKey(e.doneKeys, at), updatedAt: now } : e
2434
+ );
2435
+ }
2436
+ function applyRemove(events, id, now) {
2437
+ const buried = events.filter((e) => e.id === id || e.seriesId === id);
2438
+ return {
2439
+ events: events.filter((e) => e.id !== id && e.seriesId !== id),
2440
+ graves: buried.map((e) => ({ id: e.id, at: now }))
2441
+ };
2442
+ }
2443
+ function applySplit(events, id, at, tail, mintId, now) {
2444
+ const original = events.find((e) => e.id === id);
2445
+ if (!original) return { events, created: null };
2446
+ const split = splitRule(original, original.start, at);
2447
+ const created = {
2448
+ ...tail,
2449
+ ...split.tail,
2450
+ seriesId: "",
2451
+ seriesKey: "",
2452
+ id: mintId(),
2453
+ updatedAt: now
2454
+ };
2455
+ const seed = new Date(original.start);
2456
+ const detached = (e) => e.seriesId === id && (keyToInstant(e.seriesKey, seed) ?? -Infinity) >= at;
2457
+ return {
2458
+ events: [
2459
+ // The head, the tail and every re-pointed override are the three things
2460
+ // this touches, so they are the three it stamps. An override before the
2461
+ // cut is handed back as the same object and keeps the stamp it had.
2462
+ ...events.map(
2463
+ (e) => e.id === id ? { ...e, ...split.head, updatedAt: now } : detached(e) ? { ...e, seriesId: created.id, updatedAt: now } : e
2464
+ ),
2465
+ created
2466
+ ],
2467
+ created
2468
+ };
2469
+ }
2470
+ function applyTruncate(events, id, at, now) {
2471
+ const original = events.find((e) => e.id === id);
2472
+ if (!original) return { events, graves: [] };
2473
+ const split = splitRule(original, original.start, at);
2474
+ const seed = new Date(original.start);
2475
+ const past = (e) => e.seriesId === id && (keyToInstant(e.seriesKey, seed) ?? -Infinity) >= at;
2476
+ return {
2477
+ events: events.filter((e) => !past(e)).map((e) => e.id === id ? { ...e, ...split.head, updatedAt: now } : e),
2478
+ // Only what is past the cut: the head survives the truncation, so a grave
2479
+ // for it would delete the series a merge is meant to keep.
2480
+ graves: events.filter(past).map((e) => ({ id: e.id, at: now }))
2481
+ };
2878
2482
  }
2879
2483
 
2880
2484
  // src/lib/quickadd.ts
@@ -2905,6 +2509,16 @@ function parseDate(raw, reference) {
2905
2509
  const delta = (weekdayIndex - base.getDay() + 7) % 7 || 7;
2906
2510
  return addDays(base, delta);
2907
2511
  }
2512
+ const lastWeekday = /^last\s+([a-z]+)$/.exec(query);
2513
+ if (lastWeekday) {
2514
+ const idx = WEEKDAYS_LOWER.findIndex(
2515
+ (w) => w === lastWeekday[1] || w.slice(0, 3) === lastWeekday[1]
2516
+ );
2517
+ if (idx !== -1) {
2518
+ const delta = (base.getDay() - idx + 7) % 7 || 7;
2519
+ return addDays(base, -delta);
2520
+ }
2521
+ }
2908
2522
  const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(query);
2909
2523
  if (iso) {
2910
2524
  const year = Number(iso[1]);
@@ -2918,11 +2532,23 @@ function parseDate(raw, reference) {
2918
2532
  const resolveMonthDay = (monthStr, domStr) => {
2919
2533
  const monthIndex = MONTHS_LOWER.findIndex((m) => m === monthStr || m.slice(0, 3) === monthStr);
2920
2534
  const dom = Number(domStr);
2921
- if (monthIndex === -1 || dom < 1 || dom > daysInMonth(reference.getFullYear(), monthIndex))
2922
- return null;
2535
+ if (monthIndex === -1 || dom < 1) return null;
2923
2536
  const year = reference.getFullYear();
2924
- const candidate = new Date(year, monthIndex, dom);
2925
- return candidate.getTime() < base.getTime() ? new Date(year + 1, monthIndex, dom) : candidate;
2537
+ let best = null;
2538
+ for (const y of [year - 1, year, year + 1]) {
2539
+ if (dom > daysInMonth(y, monthIndex)) continue;
2540
+ const candidate = new Date(y, monthIndex, dom);
2541
+ if (best === null) {
2542
+ best = candidate;
2543
+ continue;
2544
+ }
2545
+ const kept = Math.abs(best.getTime() - base.getTime());
2546
+ const offered = Math.abs(candidate.getTime() - base.getTime());
2547
+ if (offered < kept || offered === kept && candidate.getTime() > base.getTime()) {
2548
+ best = candidate;
2549
+ }
2550
+ }
2551
+ return best;
2926
2552
  };
2927
2553
  const monthDay = /^([a-z]+)\s+(\d{1,2})$/.exec(query);
2928
2554
  if (monthDay) {
@@ -3164,8 +2790,467 @@ function parseQuickAdd(input, reference, now = /* @__PURE__ */ new Date(), fallb
3164
2790
  };
3165
2791
  }
3166
2792
 
2793
+ // src/lib/theme.ts
2794
+ var THEMES = ["midnight", "ember", "paper"];
2795
+ var DEFAULT_THEME = "midnight";
2796
+ function isThemeName(value) {
2797
+ return typeof value === "string" && THEMES.includes(value);
2798
+ }
2799
+
2800
+ // src/lib/places.ts
2801
+ var COORDS = {
2802
+ "Africa/Abidjan": [5.32, -4.03],
2803
+ "Africa/Accra": [5.55, -0.22],
2804
+ "Africa/Addis_Ababa": [9.03, 38.7],
2805
+ "Africa/Algiers": [36.78, 3.05],
2806
+ "Africa/Asmara": [15.33, 38.88],
2807
+ "Africa/Bamako": [12.65, -8],
2808
+ "Africa/Bangui": [4.37, 18.58],
2809
+ "Africa/Banjul": [13.47, -16.65],
2810
+ "Africa/Bissau": [11.85, -15.58],
2811
+ "Africa/Blantyre": [-15.78, 35],
2812
+ "Africa/Brazzaville": [-4.27, 15.28],
2813
+ "Africa/Bujumbura": [-3.38, 29.37],
2814
+ "Africa/Cairo": [30.05, 31.25],
2815
+ "Africa/Casablanca": [33.65, -7.58],
2816
+ "Africa/Ceuta": [35.88, -5.32],
2817
+ "Africa/Conakry": [9.52, -13.72],
2818
+ "Africa/Dakar": [14.67, -17.43],
2819
+ "Africa/Dar_es_Salaam": [-6.8, 39.28],
2820
+ "Africa/Djibouti": [11.6, 43.15],
2821
+ "Africa/Douala": [4.05, 9.7],
2822
+ "Africa/El_Aaiun": [27.15, -13.2],
2823
+ "Africa/Freetown": [8.5, -13.25],
2824
+ "Africa/Gaborone": [-24.65, 25.92],
2825
+ "Africa/Harare": [-17.83, 31.05],
2826
+ "Africa/Johannesburg": [-26.25, 28],
2827
+ "Africa/Juba": [4.85, 31.62],
2828
+ "Africa/Kampala": [0.32, 32.42],
2829
+ "Africa/Khartoum": [15.6, 32.53],
2830
+ "Africa/Kigali": [-1.95, 30.07],
2831
+ "Africa/Kinshasa": [-4.3, 15.3],
2832
+ "Africa/Lagos": [6.45, 3.4],
2833
+ "Africa/Libreville": [0.38, 9.45],
2834
+ "Africa/Lome": [6.13, 1.22],
2835
+ "Africa/Luanda": [-8.8, 13.23],
2836
+ "Africa/Lubumbashi": [-11.67, 27.47],
2837
+ "Africa/Lusaka": [-15.42, 28.28],
2838
+ "Africa/Malabo": [3.75, 8.78],
2839
+ "Africa/Maputo": [-25.97, 32.58],
2840
+ "Africa/Maseru": [-29.47, 27.5],
2841
+ "Africa/Mbabane": [-26.3, 31.1],
2842
+ "Africa/Mogadishu": [2.07, 45.37],
2843
+ "Africa/Monrovia": [6.3, -10.78],
2844
+ "Africa/Nairobi": [-1.28, 36.82],
2845
+ "Africa/Ndjamena": [12.12, 15.05],
2846
+ "Africa/Niamey": [13.52, 2.12],
2847
+ "Africa/Nouakchott": [18.1, -15.95],
2848
+ "Africa/Ouagadougou": [12.37, -1.52],
2849
+ "Africa/Porto-Novo": [6.48, 2.62],
2850
+ "Africa/Sao_Tome": [0.33, 6.73],
2851
+ "Africa/Tripoli": [32.9, 13.18],
2852
+ "Africa/Tunis": [36.8, 10.18],
2853
+ "Africa/Windhoek": [-22.57, 17.1],
2854
+ "America/Adak": [51.88, -176.66],
2855
+ "America/Anchorage": [61.22, -149.9],
2856
+ "America/Anguilla": [18.2, -63.07],
2857
+ "America/Antigua": [17.05, -61.8],
2858
+ "America/Araguaina": [-7.2, -48.2],
2859
+ "America/Argentina/Buenos_Aires": [-34.6, -58.45],
2860
+ "America/Argentina/Catamarca": [-28.47, -65.78],
2861
+ "America/Argentina/Cordoba": [-31.4, -64.18],
2862
+ "America/Argentina/Jujuy": [-24.18, -65.3],
2863
+ "America/Argentina/La_Rioja": [-29.43, -66.85],
2864
+ "America/Argentina/Mendoza": [-32.88, -68.82],
2865
+ "America/Argentina/Rio_Gallegos": [-51.63, -69.22],
2866
+ "America/Argentina/Salta": [-24.78, -65.42],
2867
+ "America/Argentina/San_Juan": [-31.53, -68.52],
2868
+ "America/Argentina/San_Luis": [-33.32, -66.35],
2869
+ "America/Argentina/Tucuman": [-26.82, -65.22],
2870
+ "America/Argentina/Ushuaia": [-54.8, -68.3],
2871
+ "America/Aruba": [12.5, -69.97],
2872
+ "America/Asuncion": [-25.27, -57.67],
2873
+ "America/Atikokan": [48.76, -91.62],
2874
+ "America/Bahia": [-12.98, -38.52],
2875
+ "America/Bahia_Banderas": [20.8, -105.25],
2876
+ "America/Barbados": [13.1, -59.62],
2877
+ "America/Belem": [-1.45, -48.48],
2878
+ "America/Belize": [17.5, -88.2],
2879
+ "America/Blanc-Sablon": [51.42, -57.12],
2880
+ "America/Boa_Vista": [2.82, -60.67],
2881
+ "America/Bogota": [4.6, -74.08],
2882
+ "America/Boise": [43.61, -116.2],
2883
+ "America/Cambridge_Bay": [69.11, -105.05],
2884
+ "America/Campo_Grande": [-20.45, -54.62],
2885
+ "America/Cancun": [21.08, -86.77],
2886
+ "America/Caracas": [10.5, -66.93],
2887
+ "America/Cayenne": [4.93, -52.33],
2888
+ "America/Cayman": [19.3, -81.38],
2889
+ "America/Chicago": [41.85, -87.65],
2890
+ "America/Chihuahua": [28.63, -106.08],
2891
+ "America/Ciudad_Juarez": [31.73, -106.48],
2892
+ "America/Costa_Rica": [9.93, -84.08],
2893
+ "America/Coyhaique": [-45.57, -72.07],
2894
+ "America/Creston": [49.1, -116.52],
2895
+ "America/Cuiaba": [-15.58, -56.08],
2896
+ "America/Curacao": [12.18, -69],
2897
+ "America/Danmarkshavn": [76.77, -18.67],
2898
+ "America/Dawson": [64.07, -139.42],
2899
+ "America/Dawson_Creek": [55.77, -120.23],
2900
+ "America/Denver": [39.74, -104.98],
2901
+ "America/Detroit": [42.33, -83.05],
2902
+ "America/Dominica": [15.3, -61.4],
2903
+ "America/Edmonton": [53.55, -113.47],
2904
+ "America/Eirunepe": [-6.67, -69.87],
2905
+ "America/El_Salvador": [13.7, -89.2],
2906
+ "America/Fort_Nelson": [58.8, -122.7],
2907
+ "America/Fortaleza": [-3.72, -38.5],
2908
+ "America/Glace_Bay": [46.2, -59.95],
2909
+ "America/Goose_Bay": [53.33, -60.42],
2910
+ "America/Grand_Turk": [21.47, -71.13],
2911
+ "America/Grenada": [12.05, -61.75],
2912
+ "America/Guadeloupe": [16.23, -61.53],
2913
+ "America/Guatemala": [14.63, -90.52],
2914
+ "America/Guayaquil": [-2.17, -79.83],
2915
+ "America/Guyana": [6.8, -58.17],
2916
+ "America/Halifax": [44.65, -63.6],
2917
+ "America/Havana": [23.13, -82.37],
2918
+ "America/Hermosillo": [29.07, -110.97],
2919
+ "America/Indiana/Indianapolis": [39.77, -86.16],
2920
+ "America/Indiana/Knox": [41.3, -86.62],
2921
+ "America/Indiana/Marengo": [38.38, -86.34],
2922
+ "America/Indiana/Petersburg": [38.49, -87.28],
2923
+ "America/Indiana/Tell_City": [37.95, -86.76],
2924
+ "America/Indiana/Vevay": [38.75, -85.07],
2925
+ "America/Indiana/Vincennes": [38.68, -87.53],
2926
+ "America/Indiana/Winamac": [41.05, -86.6],
2927
+ "America/Inuvik": [68.35, -133.72],
2928
+ "America/Iqaluit": [63.73, -68.47],
2929
+ "America/Jamaica": [17.97, -76.79],
2930
+ "America/Juneau": [58.3, -134.42],
2931
+ "America/Kentucky/Louisville": [38.25, -85.76],
2932
+ "America/Kentucky/Monticello": [36.83, -84.85],
2933
+ "America/Kralendijk": [12.15, -68.28],
2934
+ "America/La_Paz": [-16.5, -68.15],
2935
+ "America/Lima": [-12.05, -77.05],
2936
+ "America/Los_Angeles": [34.05, -118.24],
2937
+ "America/Lower_Princes": [18.05, -63.05],
2938
+ "America/Maceio": [-9.67, -35.72],
2939
+ "America/Managua": [12.15, -86.28],
2940
+ "America/Manaus": [-3.13, -60.02],
2941
+ "America/Marigot": [18.07, -63.08],
2942
+ "America/Martinique": [14.6, -61.08],
2943
+ "America/Matamoros": [25.83, -97.5],
2944
+ "America/Mazatlan": [23.22, -106.42],
2945
+ "America/Menominee": [45.11, -87.61],
2946
+ "America/Merida": [20.97, -89.62],
2947
+ "America/Metlakatla": [55.13, -131.58],
2948
+ "America/Mexico_City": [19.4, -99.15],
2949
+ "America/Miquelon": [47.05, -56.33],
2950
+ "America/Moncton": [46.1, -64.78],
2951
+ "America/Monterrey": [25.67, -100.32],
2952
+ "America/Montevideo": [-34.91, -56.21],
2953
+ "America/Montserrat": [16.72, -62.22],
2954
+ "America/Nassau": [25.08, -77.35],
2955
+ "America/New_York": [40.71, -74.01],
2956
+ "America/Nome": [64.5, -165.41],
2957
+ "America/Noronha": [-3.85, -32.42],
2958
+ "America/North_Dakota/Beulah": [47.26, -101.78],
2959
+ "America/North_Dakota/Center": [47.12, -101.3],
2960
+ "America/North_Dakota/New_Salem": [46.84, -101.41],
2961
+ "America/Nuuk": [64.18, -51.73],
2962
+ "America/Ojinaga": [29.57, -104.42],
2963
+ "America/Panama": [8.97, -79.53],
2964
+ "America/Paramaribo": [5.83, -55.17],
2965
+ "America/Phoenix": [33.45, -112.07],
2966
+ "America/Port-au-Prince": [18.53, -72.33],
2967
+ "America/Port_of_Spain": [10.65, -61.52],
2968
+ "America/Porto_Velho": [-8.77, -63.9],
2969
+ "America/Puerto_Rico": [18.47, -66.11],
2970
+ "America/Punta_Arenas": [-53.15, -70.92],
2971
+ "America/Rankin_Inlet": [62.82, -92.08],
2972
+ "America/Recife": [-8.05, -34.9],
2973
+ "America/Regina": [50.4, -104.65],
2974
+ "America/Resolute": [74.7, -94.83],
2975
+ "America/Rio_Branco": [-9.97, -67.8],
2976
+ "America/Santarem": [-2.43, -54.87],
2977
+ "America/Santiago": [-33.45, -70.67],
2978
+ "America/Santo_Domingo": [18.47, -69.9],
2979
+ "America/Sao_Paulo": [-23.53, -46.62],
2980
+ "America/Scoresbysund": [70.48, -21.97],
2981
+ "America/Sitka": [57.18, -135.3],
2982
+ "America/St_Barthelemy": [17.88, -62.85],
2983
+ "America/St_Johns": [47.57, -52.72],
2984
+ "America/St_Kitts": [17.3, -62.72],
2985
+ "America/St_Lucia": [14.02, -61],
2986
+ "America/St_Thomas": [18.35, -64.93],
2987
+ "America/St_Vincent": [13.15, -61.23],
2988
+ "America/Swift_Current": [50.28, -107.83],
2989
+ "America/Tegucigalpa": [14.1, -87.22],
2990
+ "America/Thule": [76.57, -68.78],
2991
+ "America/Tijuana": [32.53, -117.02],
2992
+ "America/Toronto": [43.65, -79.38],
2993
+ "America/Tortola": [18.45, -64.62],
2994
+ "America/Vancouver": [49.27, -123.12],
2995
+ "America/Whitehorse": [60.72, -135.05],
2996
+ "America/Winnipeg": [49.88, -97.15],
2997
+ "America/Yakutat": [59.55, -139.73],
2998
+ "Antarctica/Casey": [-66.28, 110.52],
2999
+ "Antarctica/Davis": [-68.58, 77.97],
3000
+ "Antarctica/DumontDUrville": [-66.67, 140.02],
3001
+ "Antarctica/Macquarie": [-54.5, 158.95],
3002
+ "Antarctica/Mawson": [-67.6, 62.88],
3003
+ "Antarctica/McMurdo": [-77.83, 166.6],
3004
+ "Antarctica/Palmer": [-64.8, -64.1],
3005
+ "Antarctica/Rothera": [-67.57, -68.13],
3006
+ "Antarctica/Syowa": [-69.01, 39.59],
3007
+ "Antarctica/Troll": [-72.01, 2.53],
3008
+ "Antarctica/Vostok": [-78.4, 106.9],
3009
+ "Arctic/Longyearbyen": [78, 16],
3010
+ "Asia/Aden": [12.75, 45.2],
3011
+ "Asia/Almaty": [43.25, 76.95],
3012
+ "Asia/Amman": [31.95, 35.93],
3013
+ "Asia/Anadyr": [64.75, 177.48],
3014
+ "Asia/Aqtau": [44.52, 50.27],
3015
+ "Asia/Aqtobe": [50.28, 57.17],
3016
+ "Asia/Ashgabat": [37.95, 58.38],
3017
+ "Asia/Atyrau": [47.12, 51.93],
3018
+ "Asia/Baghdad": [33.35, 44.42],
3019
+ "Asia/Bahrain": [26.38, 50.58],
3020
+ "Asia/Baku": [40.38, 49.85],
3021
+ "Asia/Bangkok": [13.75, 100.52],
3022
+ "Asia/Barnaul": [53.37, 83.75],
3023
+ "Asia/Beirut": [33.88, 35.5],
3024
+ "Asia/Bishkek": [42.9, 74.6],
3025
+ "Asia/Brunei": [4.93, 114.92],
3026
+ "Asia/Chita": [52.05, 113.47],
3027
+ "Asia/Colombo": [6.93, 79.85],
3028
+ "Asia/Damascus": [33.5, 36.3],
3029
+ "Asia/Dhaka": [23.72, 90.42],
3030
+ "Asia/Dili": [-8.55, 125.58],
3031
+ "Asia/Dubai": [25.3, 55.3],
3032
+ "Asia/Dushanbe": [38.58, 68.8],
3033
+ "Asia/Famagusta": [35.12, 33.95],
3034
+ "Asia/Gaza": [31.5, 34.47],
3035
+ "Asia/Hebron": [31.53, 35.09],
3036
+ "Asia/Ho_Chi_Minh": [10.75, 106.67],
3037
+ "Asia/Hong_Kong": [22.28, 114.15],
3038
+ "Asia/Hovd": [48.02, 91.65],
3039
+ "Asia/Irkutsk": [52.27, 104.33],
3040
+ "Asia/Jakarta": [-6.17, 106.8],
3041
+ "Asia/Jayapura": [-2.53, 140.7],
3042
+ "Asia/Jerusalem": [31.78, 35.22],
3043
+ "Asia/Kabul": [34.52, 69.2],
3044
+ "Asia/Kamchatka": [53.02, 158.65],
3045
+ "Asia/Karachi": [24.87, 67.05],
3046
+ "Asia/Kathmandu": [27.72, 85.32],
3047
+ "Asia/Khandyga": [62.66, 135.55],
3048
+ "Asia/Kolkata": [22.53, 88.37],
3049
+ "Asia/Krasnoyarsk": [56.02, 92.83],
3050
+ "Asia/Kuala_Lumpur": [3.17, 101.7],
3051
+ "Asia/Kuching": [1.55, 110.33],
3052
+ "Asia/Kuwait": [29.33, 47.98],
3053
+ "Asia/Macau": [22.2, 113.54],
3054
+ "Asia/Magadan": [59.57, 150.8],
3055
+ "Asia/Makassar": [-5.12, 119.4],
3056
+ "Asia/Manila": [14.59, 120.97],
3057
+ "Asia/Muscat": [23.6, 58.58],
3058
+ "Asia/Nicosia": [35.17, 33.37],
3059
+ "Asia/Novokuznetsk": [53.75, 87.12],
3060
+ "Asia/Novosibirsk": [55.03, 82.92],
3061
+ "Asia/Omsk": [55, 73.4],
3062
+ "Asia/Oral": [51.22, 51.35],
3063
+ "Asia/Phnom_Penh": [11.55, 104.92],
3064
+ "Asia/Pontianak": [-0.03, 109.33],
3065
+ "Asia/Pyongyang": [39.02, 125.75],
3066
+ "Asia/Qatar": [25.28, 51.53],
3067
+ "Asia/Qostanay": [53.2, 63.62],
3068
+ "Asia/Qyzylorda": [44.8, 65.47],
3069
+ "Asia/Riyadh": [24.63, 46.72],
3070
+ "Asia/Sakhalin": [46.97, 142.7],
3071
+ "Asia/Samarkand": [39.67, 66.8],
3072
+ "Asia/Seoul": [37.55, 126.97],
3073
+ "Asia/Shanghai": [31.23, 121.47],
3074
+ "Asia/Singapore": [1.28, 103.85],
3075
+ "Asia/Srednekolymsk": [67.47, 153.72],
3076
+ "Asia/Taipei": [25.05, 121.5],
3077
+ "Asia/Tashkent": [41.33, 69.3],
3078
+ "Asia/Tbilisi": [41.72, 44.82],
3079
+ "Asia/Tehran": [35.67, 51.43],
3080
+ "Asia/Thimphu": [27.47, 89.65],
3081
+ "Asia/Tokyo": [35.65, 139.74],
3082
+ "Asia/Tomsk": [56.5, 84.97],
3083
+ "Asia/Ulaanbaatar": [47.92, 106.88],
3084
+ "Asia/Urumqi": [43.8, 87.58],
3085
+ "Asia/Ust-Nera": [64.56, 143.23],
3086
+ "Asia/Vientiane": [17.97, 102.6],
3087
+ "Asia/Vladivostok": [43.17, 131.93],
3088
+ "Asia/Yakutsk": [62, 129.67],
3089
+ "Asia/Yangon": [16.78, 96.17],
3090
+ "Asia/Yekaterinburg": [56.85, 60.6],
3091
+ "Asia/Yerevan": [40.18, 44.5],
3092
+ "Atlantic/Azores": [37.73, -25.67],
3093
+ "Atlantic/Bermuda": [32.28, -64.77],
3094
+ "Atlantic/Canary": [28.1, -15.4],
3095
+ "Atlantic/Cape_Verde": [14.92, -23.52],
3096
+ "Atlantic/Faroe": [62.02, -6.77],
3097
+ "Atlantic/Madeira": [32.63, -16.9],
3098
+ "Atlantic/Reykjavik": [64.15, -21.85],
3099
+ "Atlantic/South_Georgia": [-54.27, -36.53],
3100
+ "Atlantic/St_Helena": [-15.92, -5.7],
3101
+ "Atlantic/Stanley": [-51.7, -57.85],
3102
+ "Australia/Adelaide": [-34.92, 138.58],
3103
+ "Australia/Brisbane": [-27.47, 153.03],
3104
+ "Australia/Broken_Hill": [-31.95, 141.45],
3105
+ "Australia/Darwin": [-12.47, 130.83],
3106
+ "Australia/Eucla": [-31.72, 128.87],
3107
+ "Australia/Hobart": [-42.88, 147.32],
3108
+ "Australia/Lindeman": [-20.27, 149],
3109
+ "Australia/Lord_Howe": [-31.55, 159.08],
3110
+ "Australia/Melbourne": [-37.82, 144.97],
3111
+ "Australia/Perth": [-31.95, 115.85],
3112
+ "Australia/Sydney": [-33.87, 151.22],
3113
+ "Europe/Amsterdam": [52.37, 4.9],
3114
+ "Europe/Andorra": [42.5, 1.52],
3115
+ "Europe/Astrakhan": [46.35, 48.05],
3116
+ "Europe/Athens": [37.97, 23.72],
3117
+ "Europe/Belgrade": [44.83, 20.5],
3118
+ "Europe/Berlin": [52.5, 13.37],
3119
+ "Europe/Bratislava": [48.15, 17.12],
3120
+ "Europe/Brussels": [50.83, 4.33],
3121
+ "Europe/Bucharest": [44.43, 26.1],
3122
+ "Europe/Budapest": [47.5, 19.08],
3123
+ "Europe/Busingen": [47.7, 8.68],
3124
+ "Europe/Chisinau": [47, 28.83],
3125
+ "Europe/Copenhagen": [55.67, 12.58],
3126
+ "Europe/Dublin": [53.33, -6.25],
3127
+ "Europe/Gibraltar": [36.13, -5.35],
3128
+ "Europe/Guernsey": [49.45, -2.54],
3129
+ "Europe/Helsinki": [60.17, 24.97],
3130
+ "Europe/Isle_of_Man": [54.15, -4.47],
3131
+ "Europe/Istanbul": [41.02, 28.97],
3132
+ "Europe/Jersey": [49.18, -2.11],
3133
+ "Europe/Kaliningrad": [54.72, 20.5],
3134
+ "Europe/Kirov": [58.6, 49.65],
3135
+ "Europe/Kyiv": [50.43, 30.52],
3136
+ "Europe/Lisbon": [38.72, -9.13],
3137
+ "Europe/Ljubljana": [46.05, 14.52],
3138
+ "Europe/London": [51.51, -0.13],
3139
+ "Europe/Luxembourg": [49.6, 6.15],
3140
+ "Europe/Madrid": [40.4, -3.68],
3141
+ "Europe/Malta": [35.9, 14.52],
3142
+ "Europe/Mariehamn": [60.1, 19.95],
3143
+ "Europe/Minsk": [53.9, 27.57],
3144
+ "Europe/Monaco": [43.7, 7.38],
3145
+ "Europe/Moscow": [55.76, 37.62],
3146
+ "Europe/Oslo": [59.92, 10.75],
3147
+ "Europe/Paris": [48.87, 2.33],
3148
+ "Europe/Podgorica": [42.43, 19.27],
3149
+ "Europe/Prague": [50.08, 14.43],
3150
+ "Europe/Riga": [56.95, 24.1],
3151
+ "Europe/Rome": [41.9, 12.48],
3152
+ "Europe/Samara": [53.2, 50.15],
3153
+ "Europe/San_Marino": [43.92, 12.47],
3154
+ "Europe/Sarajevo": [43.87, 18.42],
3155
+ "Europe/Saratov": [51.57, 46.03],
3156
+ "Europe/Simferopol": [44.95, 34.1],
3157
+ "Europe/Skopje": [41.98, 21.43],
3158
+ "Europe/Sofia": [42.68, 23.32],
3159
+ "Europe/Stockholm": [59.33, 18.05],
3160
+ "Europe/Tallinn": [59.42, 24.75],
3161
+ "Europe/Tirane": [41.33, 19.83],
3162
+ "Europe/Ulyanovsk": [54.33, 48.4],
3163
+ "Europe/Vaduz": [47.15, 9.52],
3164
+ "Europe/Vatican": [41.9, 12.45],
3165
+ "Europe/Vienna": [48.22, 16.33],
3166
+ "Europe/Vilnius": [54.68, 25.32],
3167
+ "Europe/Volgograd": [48.73, 44.42],
3168
+ "Europe/Warsaw": [52.25, 21],
3169
+ "Europe/Zagreb": [45.8, 15.97],
3170
+ "Europe/Zurich": [47.38, 8.53],
3171
+ "Indian/Antananarivo": [-18.92, 47.52],
3172
+ "Indian/Chagos": [-7.33, 72.42],
3173
+ "Indian/Christmas": [-10.42, 105.72],
3174
+ "Indian/Cocos": [-12.17, 96.92],
3175
+ "Indian/Comoro": [-11.68, 43.27],
3176
+ "Indian/Kerguelen": [-49.35, 70.22],
3177
+ "Indian/Mahe": [-4.67, 55.47],
3178
+ "Indian/Maldives": [4.17, 73.5],
3179
+ "Indian/Mauritius": [-20.17, 57.5],
3180
+ "Indian/Mayotte": [-12.78, 45.23],
3181
+ "Indian/Reunion": [-20.87, 55.47],
3182
+ "Pacific/Apia": [-13.83, -171.73],
3183
+ "Pacific/Auckland": [-36.87, 174.77],
3184
+ "Pacific/Bougainville": [-6.22, 155.57],
3185
+ "Pacific/Chatham": [-43.95, -176.55],
3186
+ "Pacific/Chuuk": [7.42, 151.78],
3187
+ "Pacific/Easter": [-27.15, -109.43],
3188
+ "Pacific/Efate": [-17.67, 168.42],
3189
+ "Pacific/Fakaofo": [-9.37, -171.23],
3190
+ "Pacific/Fiji": [-18.13, 178.42],
3191
+ "Pacific/Funafuti": [-8.52, 179.22],
3192
+ "Pacific/Galapagos": [-0.9, -89.6],
3193
+ "Pacific/Gambier": [-23.13, -134.95],
3194
+ "Pacific/Guadalcanal": [-9.53, 160.2],
3195
+ "Pacific/Guam": [13.47, 144.75],
3196
+ "Pacific/Honolulu": [21.31, -157.86],
3197
+ "Pacific/Kanton": [-2.78, -171.72],
3198
+ "Pacific/Kiritimati": [1.87, -157.33],
3199
+ "Pacific/Kosrae": [5.32, 162.98],
3200
+ "Pacific/Kwajalein": [9.08, 167.33],
3201
+ "Pacific/Majuro": [7.15, 171.2],
3202
+ "Pacific/Marquesas": [-9, -139.5],
3203
+ "Pacific/Midway": [28.22, -177.37],
3204
+ "Pacific/Nauru": [-0.52, 166.92],
3205
+ "Pacific/Niue": [-19.02, -169.92],
3206
+ "Pacific/Norfolk": [-29.05, 167.97],
3207
+ "Pacific/Noumea": [-22.27, 166.45],
3208
+ "Pacific/Pago_Pago": [-14.27, -170.7],
3209
+ "Pacific/Palau": [7.33, 134.48],
3210
+ "Pacific/Pitcairn": [-25.07, -130.08],
3211
+ "Pacific/Pohnpei": [6.97, 158.22],
3212
+ "Pacific/Port_Moresby": [-9.5, 147.17],
3213
+ "Pacific/Rarotonga": [-21.23, -159.77],
3214
+ "Pacific/Saipan": [15.2, 145.75],
3215
+ "Pacific/Tahiti": [-17.53, -149.57],
3216
+ "Pacific/Tarawa": [1.42, 173],
3217
+ "Pacific/Tongatapu": [-21.13, -175.2],
3218
+ "Pacific/Wake": [19.28, 166.62],
3219
+ "Pacific/Wallis": [-13.3, -176.17]
3220
+ };
3221
+ var ALIASES = {
3222
+ "Africa/Asmera": "Africa/Asmara",
3223
+ "America/Buenos_Aires": "America/Argentina/Buenos_Aires",
3224
+ "America/Catamarca": "America/Argentina/Catamarca",
3225
+ "America/Coral_Harbour": "America/Atikokan",
3226
+ "America/Cordoba": "America/Argentina/Cordoba",
3227
+ "America/Godthab": "America/Nuuk",
3228
+ "America/Indianapolis": "America/Indiana/Indianapolis",
3229
+ "America/Jujuy": "America/Argentina/Jujuy",
3230
+ "America/Louisville": "America/Kentucky/Louisville",
3231
+ "America/Mendoza": "America/Argentina/Mendoza",
3232
+ "Asia/Calcutta": "Asia/Kolkata",
3233
+ "Asia/Katmandu": "Asia/Kathmandu",
3234
+ "Asia/Rangoon": "Asia/Yangon",
3235
+ "Asia/Saigon": "Asia/Ho_Chi_Minh",
3236
+ "Atlantic/Faeroe": "Atlantic/Faroe",
3237
+ "Europe/Kiev": "Europe/Kyiv",
3238
+ "Pacific/Enderbury": "Pacific/Kanton",
3239
+ "Pacific/Ponape": "Pacific/Pohnpei",
3240
+ "Pacific/Truk": "Pacific/Chuuk"
3241
+ };
3242
+ function placeForZone(zone) {
3243
+ const coords = COORDS[zone] ?? COORDS[ALIASES[zone]];
3244
+ if (!coords) return null;
3245
+ return { zone, lat: coords[0], lon: coords[1], label: zoneLabel(zone) };
3246
+ }
3247
+ var PLACE_OPTIONS = Object.keys(COORDS).map((zone) => ({ value: zone, label: zoneLabel(zone) })).sort((a, b) => a.label.localeCompare(b.label));
3248
+
3167
3249
  // src/lib/store.ts
3168
3250
  var DEFAULT_SETTINGS = {
3251
+ // Midnight is not "a preset that happens to be first": it is the app every
3252
+ // existing user has, so it is the default and the other two are opt-in.
3253
+ theme: DEFAULT_THEME,
3169
3254
  // The 0–23 face. It is the one that matches the instrument: a revolution is
3170
3255
  // a day here, not half of one, so an hour has exactly one place on it and
3171
3256
  // `13` is where `13` is. AM/PM is a preference, and it is one click away.
@@ -3285,9 +3370,33 @@ var DEFAULT_SETTINGS = {
3285
3370
  wakingTo: DEFAULT_WAKING.to
3286
3371
  };
3287
3372
  var DEFAULT_CALENDARS = [
3288
- { id: "personal", name: "Personal", color: PALETTE[0], icon: "house", visible: true },
3289
- { id: "work", name: "Work", color: PALETTE[3], icon: "briefcase", visible: true },
3290
- { id: "family", name: "Family", color: PALETTE[5], icon: "users", visible: true }
3373
+ {
3374
+ id: "personal",
3375
+ name: "Personal",
3376
+ color: PALETTE[0],
3377
+ icon: "house",
3378
+ visible: true,
3379
+ remindLead: REMIND_INHERIT,
3380
+ updatedAt: 0
3381
+ },
3382
+ {
3383
+ id: "work",
3384
+ name: "Work",
3385
+ color: PALETTE[3],
3386
+ icon: "briefcase",
3387
+ visible: true,
3388
+ remindLead: REMIND_INHERIT,
3389
+ updatedAt: 0
3390
+ },
3391
+ {
3392
+ id: "family",
3393
+ name: "Family",
3394
+ color: PALETTE[5],
3395
+ icon: "users",
3396
+ visible: true,
3397
+ remindLead: REMIND_INHERIT,
3398
+ updatedAt: 0
3399
+ }
3291
3400
  ];
3292
3401
  function newId() {
3293
3402
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -3330,10 +3439,26 @@ function draftEvent(day, startMin, endMin, calendarId, repeat = "never") {
3330
3439
  // somebody else wrote.
3331
3440
  organizer: null,
3332
3441
  attendees: [],
3333
- attendeeCount: 0
3442
+ attendeeCount: 0,
3443
+ // A draft has never been saved, so it has never been changed. `create` and
3444
+ // `update` are where a record gets its stamp; a `0` reaching storage would
3445
+ // be an event that predates stamping, which is the conservative loser in
3446
+ // every merge rather than a lie.
3447
+ updatedAt: 0
3334
3448
  };
3335
3449
  }
3336
3450
 
3451
+ // src/lib/ics.ts
3452
+ var encoder = new TextEncoder();
3453
+ function fnv1a(source) {
3454
+ let h = 2166136261;
3455
+ for (let i = 0; i < source.length; i++) {
3456
+ h ^= source.charCodeAt(i);
3457
+ h = Math.imul(h, 16777619) >>> 0;
3458
+ }
3459
+ return h.toString(36);
3460
+ }
3461
+
3337
3462
  // src/lib/backup.ts
3338
3463
  function readSettings(value) {
3339
3464
  if (!isRecord(value)) return DEFAULT_SETTINGS;
@@ -3343,6 +3468,10 @@ function readSettings(value) {
3343
3468
  // there are exactly two spellings and both are written out by every
3344
3469
  // version that has ever had the setting.
3345
3470
  clock: value.clock === "12h" || value.clock === "24h" ? value.clock : DEFAULT_SETTINGS.clock,
3471
+ // A preset this build does not carry resets rather than half-applies:
3472
+ // `sane()` would pin it anyway, but a restored calendar should not spend
3473
+ // one commit looking broken first.
3474
+ theme: isThemeName(value.theme) ? value.theme : DEFAULT_SETTINGS.theme,
3346
3475
  secondZone: typeof value.secondZone === "string" ? value.secondZone : "",
3347
3476
  // The zone the browser that wrote this backup was in. Carried rather than
3348
3477
  // dropped, and deliberately not replaced with the reader's own: a Berlin
@@ -3505,7 +3634,12 @@ function readEvent(value) {
3505
3634
  // did — clamped up to the list it labels, since a blob understating it
3506
3635
  // would print a negative remainder.
3507
3636
  organizer: readPerson(value.organizer),
3508
- ...readGuests(value.attendees, value.attendeeCount)
3637
+ ...readGuests(value.attendees, value.attendeeCount),
3638
+ // What a legal stamp is lives in `events.ts`, beside the graves it is
3639
+ // compared against. `0` for anything unreadable and for every backup taken
3640
+ // before stamping existed, which is what tells a merge that a document is
3641
+ // not one it can merge.
3642
+ updatedAt: readStamp(value.updatedAt)
3509
3643
  };
3510
3644
  }
3511
3645
  function readCalendar(value) {
@@ -3523,7 +3657,14 @@ function readCalendar(value) {
3523
3657
  // again should not lose the mark — but an unbounded string is a paste, and
3524
3658
  // an over-long *name* is now dropped rather than cut into junk.
3525
3659
  icon: readMark(icon),
3660
+ // Same validator as an event's lead, same reason: the middle level of the
3661
+ // inherit chain arrives from the same untrusted JSON the top one does, and
3662
+ // a second opinion about what a legal lead is would be `readRecurrence`'s
3663
+ // documented mistake. Absent reads as inheriting, which is what every
3664
+ // calendar written before this field existed already did.
3665
+ remindLead: readRemindLead(value.remindLead),
3526
3666
  visible: value.visible !== false,
3667
+ updatedAt: readStamp(value.updatedAt),
3527
3668
  ...feedUrl ? { feed: { url: feedUrl, fetchedAt: 0, error: "" } } : {}
3528
3669
  };
3529
3670
  }
@@ -3542,12 +3683,17 @@ function readBackup(parsed) {
3542
3683
  return e.seriesId && !seriesIds.has(e.seriesId) ? { ...parented, seriesId: "", seriesKey: "" } : parented;
3543
3684
  }),
3544
3685
  calendars,
3545
- settings: readSettings(parsed.settings)
3686
+ settings: readSettings(parsed.settings),
3687
+ // Not through `readSettings`: this is bookkeeping about the object rather
3688
+ // than a field of it, and a reader that clamped it into `Settings` would
3689
+ // have to teach every one of the thirty-eight about a stamp.
3690
+ settingsAt: readStamp(parsed.settingsAt),
3691
+ graves: readGraves(parsed.graves)
3546
3692
  };
3547
3693
  }
3548
3694
 
3549
3695
  // src/lib/mirror.ts
3550
- var MIRROR_VERSION = 1;
3696
+ var MIRROR_VERSION = 2;
3551
3697
  function settled(calendars) {
3552
3698
  return calendars.map(
3553
3699
  (c) => c.feed ? { ...c, feed: { ...c.feed, fetchedAt: 0, error: "" } } : c
@@ -3562,7 +3708,9 @@ function serialiseMirror(s, rev, zone = localZone()) {
3562
3708
  rev,
3563
3709
  events: s.events,
3564
3710
  calendars: settled(s.calendars),
3565
- settings: s.settings
3711
+ settings: s.settings,
3712
+ settingsAt: s.settingsAt,
3713
+ graves: s.graves
3566
3714
  };
3567
3715
  return `${JSON.stringify(file, null, 2)}
3568
3716
  `;
@@ -3581,6 +3729,11 @@ function readMirrorFile(text) {
3581
3729
  events: read.events,
3582
3730
  calendars: read.calendars,
3583
3731
  settings: read.settings,
3732
+ // Both through `readBackup`, which is the boundary that already assumes
3733
+ // hostile input. A file that carries neither reads as a document that
3734
+ // predates stamping, and `mirrorMerges` declines to merge it.
3735
+ settingsAt: read.settingsAt,
3736
+ graves: read.graves,
3584
3737
  rev: typeof envelope.rev === "number" && Number.isFinite(envelope.rev) ? envelope.rev : 0,
3585
3738
  // A plain `.json` backup has no zone. Absent means "written by something
3586
3739
  // that did not say", which the reader treats as agreeing with it — the
@@ -3593,30 +3746,128 @@ function readMirrorFile(text) {
3593
3746
  };
3594
3747
  }
3595
3748
 
3749
+ // src/lib/vault.ts
3750
+ var VAULT_VERSION = 1;
3751
+ var PBKDF2_ITERATIONS = 6e5;
3752
+ var VERSION_BYTES = 1;
3753
+ var SALT_BYTES = 16;
3754
+ var IV_BYTES = 12;
3755
+ var TAG_BYTES = 16;
3756
+ var HEADER_BYTES = VERSION_BYTES + SALT_BYTES + IV_BYTES;
3757
+ var MIN_BYTES = HEADER_BYTES + TAG_BYTES;
3758
+ var utf8 = new TextEncoder();
3759
+ var utf8Reader = new TextDecoder();
3760
+ var WrongPassphrase = class extends Error {
3761
+ };
3762
+ var UnreadablePayload = class extends Error {
3763
+ };
3764
+ function parse(payload) {
3765
+ const bytes = base64UrlToBytes(payload);
3766
+ if (!bytes || bytes.length < MIN_BYTES) return null;
3767
+ if (bytes[0] < 1 || bytes[0] > VAULT_VERSION) return null;
3768
+ return {
3769
+ salt: bytes.subarray(VERSION_BYTES, VERSION_BYTES + SALT_BYTES),
3770
+ iv: bytes.subarray(VERSION_BYTES + SALT_BYTES, HEADER_BYTES),
3771
+ body: bytes.subarray(HEADER_BYTES)
3772
+ };
3773
+ }
3774
+ function saltOf(payload) {
3775
+ const envelope = parse(payload);
3776
+ return envelope ? new Uint8Array(envelope.salt) : null;
3777
+ }
3778
+ async function deriveBits(passphrase, salt) {
3779
+ const material = await crypto.subtle.importKey("raw", utf8.encode(passphrase), "PBKDF2", false, [
3780
+ "deriveBits"
3781
+ ]);
3782
+ const bits = await crypto.subtle.deriveBits(
3783
+ { name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
3784
+ material,
3785
+ 256
3786
+ );
3787
+ return new Uint8Array(bits);
3788
+ }
3789
+ async function restoreKeyed(raw, salt) {
3790
+ const key = await crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, [
3791
+ "encrypt",
3792
+ "decrypt"
3793
+ ]);
3794
+ return { key, salt };
3795
+ }
3796
+ function vaultOf({ key, salt }) {
3797
+ return {
3798
+ async seal(plaintext) {
3799
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
3800
+ const cipher = new Uint8Array(
3801
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, utf8.encode(plaintext))
3802
+ );
3803
+ const out = new Uint8Array(HEADER_BYTES + cipher.length);
3804
+ out[0] = VAULT_VERSION;
3805
+ out.set(salt, VERSION_BYTES);
3806
+ out.set(iv, VERSION_BYTES + SALT_BYTES);
3807
+ out.set(cipher, HEADER_BYTES);
3808
+ return bytesToBase64Url(out);
3809
+ },
3810
+ async open(payload) {
3811
+ const envelope = parse(payload);
3812
+ if (!envelope) throw new UnreadablePayload("not a vault payload this build can read");
3813
+ let plain;
3814
+ try {
3815
+ plain = await crypto.subtle.decrypt(
3816
+ { name: "AES-GCM", iv: envelope.iv },
3817
+ key,
3818
+ envelope.body
3819
+ );
3820
+ } catch {
3821
+ throw new WrongPassphrase("passphrase does not open this payload");
3822
+ }
3823
+ return utf8Reader.decode(plain);
3824
+ }
3825
+ };
3826
+ }
3827
+
3828
+ // src/lib/cloudLink.ts
3829
+ function readCloudRow(value) {
3830
+ if (typeof value !== "object" || value === null) return null;
3831
+ const row = value;
3832
+ if (typeof row.payload !== "string" || row.payload === "") return null;
3833
+ return {
3834
+ payload: row.payload,
3835
+ rev: typeof row.rev === "number" && Number.isFinite(row.rev) ? row.rev : 0,
3836
+ zone: typeof row.zone === "string" ? row.zone : "",
3837
+ version: typeof row.version === "number" && Number.isFinite(row.version) ? row.version : MIRROR_VERSION
3838
+ };
3839
+ }
3840
+ var LOCKED = "That passphrase does not open the cloud copy. Nothing was written \u2014 this device's calendar is untouched.";
3841
+ var writing = Promise.resolve();
3842
+
3596
3843
  // mcp/file.ts
3844
+ import { randomBytes } from "node:crypto";
3845
+ import { open, readFile, rename, rm, stat } from "node:fs/promises";
3846
+ import { dirname, join } from "node:path";
3847
+ import { setTimeout as sleep } from "node:timers/promises";
3597
3848
  var RefusalError = class extends Error {
3598
3849
  };
3599
3850
  function resolveFilePath(argv2, env) {
3600
3851
  const flagIndex = argv2.indexOf("--file");
3601
- const flagValue = flagIndex >= 0 ? argv2[flagIndex + 1] : void 0;
3602
- const path2 = flagValue || env.CIRCAL_FILE;
3603
- if (!path2) {
3852
+ const flagValue2 = flagIndex >= 0 ? argv2[flagIndex + 1] : void 0;
3853
+ const path = flagValue2 || env.CIRCAL_FILE;
3854
+ if (!path) {
3604
3855
  throw new Error(
3605
3856
  "circal-mcp needs a mirror file to read and write. Set the CIRCAL_FILE environment variable or pass --file <path> on the command line. Neither was given."
3606
3857
  );
3607
3858
  }
3608
- return path2;
3859
+ return path;
3609
3860
  }
3610
- async function readMirror(path2) {
3611
- const text = await readFile(path2, "utf8");
3861
+ async function readMirror(path) {
3862
+ const text = await readFile(path, "utf8");
3612
3863
  const contents = readMirrorFile(text);
3613
3864
  if (!contents) {
3614
- throw new Error(`${path2} is not a circal mirror file (or a circal .json backup).`);
3865
+ throw new Error(`${path} is not a circal mirror file (or a circal .json backup).`);
3615
3866
  }
3616
3867
  return contents;
3617
3868
  }
3618
- async function atomicWrite(path2, body) {
3619
- const dir = dirname(path2);
3869
+ async function atomicWrite(path, body) {
3870
+ const dir = dirname(path);
3620
3871
  const tmp = join(dir, `.circal-mcp-${randomBytes(6).toString("hex")}.tmp`);
3621
3872
  try {
3622
3873
  const file = await open(tmp, "w");
@@ -3626,7 +3877,7 @@ async function atomicWrite(path2, body) {
3626
3877
  } finally {
3627
3878
  await file.close();
3628
3879
  }
3629
- await rename(tmp, path2);
3880
+ await rename(tmp, path);
3630
3881
  } catch (error) {
3631
3882
  await rm(tmp, { force: true });
3632
3883
  throw error;
@@ -3660,33 +3911,82 @@ function assertVersionAllowsMutation(contents) {
3660
3911
  `Refusing to write: the file's format is version ${contents.version} and this server understands ${MIRROR_VERSION}. Writing it would drop every field this build has no name for. Rebuild circal-mcp from the matching source (pnpm build:mcp) and retry.`
3661
3912
  );
3662
3913
  }
3663
- async function loadForRead(ctx2) {
3664
- const contents = await readMirror(ctx2.path);
3914
+ async function loadForRead(ctx) {
3915
+ const contents = await readMirror(ctx.path);
3665
3916
  return { contents, warning: zoneWarning(contents) };
3666
3917
  }
3667
- async function casWriteOnce(ctx2, mutate) {
3668
- for (let attempt = 0; attempt < 2; attempt++) {
3669
- const before = await readMirror(ctx2.path);
3670
- assertVersionAllowsMutation(before);
3671
- assertZoneAllowsMutation(before, ctx2.env);
3672
- const { result, summary } = mutate(before);
3673
- const after = await readMirror(ctx2.path);
3674
- if (after.rev !== before.rev) {
3675
- if (attempt === 1) {
3676
- throw new Error(
3677
- `The file moved under this write: read at rev ${before.rev}, still found rev ${after.rev} after retrying once. Something else is writing it \u2014 try again.`
3678
- );
3918
+ function fileStore(ctx) {
3919
+ return {
3920
+ load: () => loadForRead(ctx),
3921
+ write: (mutate) => casWrite(ctx, mutate)
3922
+ };
3923
+ }
3924
+ var LOCK_STALE_MS = 1e4;
3925
+ var LOCK_WAIT_MS = 2e3;
3926
+ var LOCK_POLL_MS = 100;
3927
+ async function acquireLock(path) {
3928
+ const lock = `${path}.lock`;
3929
+ const deadline = Date.now() + LOCK_WAIT_MS;
3930
+ for (; ; ) {
3931
+ try {
3932
+ const file = await open(lock, "wx");
3933
+ try {
3934
+ await file.writeFile(`${process.pid} ${(/* @__PURE__ */ new Date()).toISOString()}
3935
+ `, "utf8");
3936
+ } finally {
3937
+ await file.close();
3938
+ }
3939
+ return lock;
3940
+ } catch (error) {
3941
+ if (error.code !== "EEXIST") throw error;
3942
+ }
3943
+ try {
3944
+ if (Date.now() - (await stat(lock)).mtimeMs > LOCK_STALE_MS) {
3945
+ await rm(lock, { force: true });
3946
+ continue;
3679
3947
  }
3948
+ } catch {
3680
3949
  continue;
3681
3950
  }
3682
- await atomicWrite(ctx2.path, serialiseMirror(result, after.rev + 1, after.zone || localZone()));
3683
- return summary;
3951
+ if (Date.now() >= deadline) {
3952
+ throw new Error(
3953
+ `Another writer holds ${lock}. If nothing else is writing this calendar, delete that file and retry.`
3954
+ );
3955
+ }
3956
+ await sleep(LOCK_POLL_MS);
3957
+ }
3958
+ }
3959
+ async function casWriteOnce(ctx, mutate) {
3960
+ const lock = await acquireLock(ctx.path);
3961
+ try {
3962
+ for (let attempt = 0; attempt < 2; attempt++) {
3963
+ const before = await readMirror(ctx.path);
3964
+ assertVersionAllowsMutation(before);
3965
+ assertZoneAllowsMutation(before, ctx.env);
3966
+ const { result, summary } = mutate(before);
3967
+ const after = await readMirror(ctx.path);
3968
+ if (after.rev !== before.rev) {
3969
+ if (attempt === 1) {
3970
+ throw new Error(
3971
+ `The file moved under this write: read at rev ${before.rev}, still found rev ${after.rev} after retrying once. Something else is writing it \u2014 try again.`
3972
+ );
3973
+ }
3974
+ continue;
3975
+ }
3976
+ await atomicWrite(
3977
+ ctx.path,
3978
+ serialiseMirror(result, after.rev + 1, after.zone || localZone())
3979
+ );
3980
+ return summary;
3981
+ }
3982
+ throw new Error("unreachable");
3983
+ } finally {
3984
+ await rm(lock, { force: true });
3684
3985
  }
3685
- throw new Error("unreachable");
3686
3986
  }
3687
3987
  var writeQueue = Promise.resolve();
3688
- function casWrite(ctx2, mutate) {
3689
- const run = writeQueue.then(() => casWriteOnce(ctx2, mutate));
3988
+ function casWrite(ctx, mutate) {
3989
+ const run = writeQueue.then(() => casWriteOnce(ctx, mutate));
3690
3990
  writeQueue = run.then(
3691
3991
  () => void 0,
3692
3992
  () => void 0
@@ -3694,33 +3994,409 @@ function casWrite(ctx2, mutate) {
3694
3994
  return run;
3695
3995
  }
3696
3996
 
3997
+ // mcp/cloud.ts
3998
+ function credentialsPath(env = process.env) {
3999
+ if (env.CIRCAL_CLOUD_CREDENTIALS) return env.CIRCAL_CLOUD_CREDENTIALS;
4000
+ const home = homedir();
4001
+ if (process.platform === "darwin")
4002
+ return join2(home, "Library", "Application Support", "circal", "cloud.json");
4003
+ if (process.platform === "win32")
4004
+ return join2(env.APPDATA || join2(home, "AppData", "Roaming"), "circal", "cloud.json");
4005
+ return join2(env.XDG_CONFIG_HOME || join2(home, ".config"), "circal", "cloud.json");
4006
+ }
4007
+ async function readCredentials(path) {
4008
+ let text;
4009
+ try {
4010
+ text = await readFile2(path, "utf8");
4011
+ } catch {
4012
+ return null;
4013
+ }
4014
+ let parsed;
4015
+ try {
4016
+ parsed = JSON.parse(text);
4017
+ } catch {
4018
+ return null;
4019
+ }
4020
+ if (typeof parsed !== "object" || parsed === null) return null;
4021
+ const record = parsed;
4022
+ const strings = [
4023
+ "url",
4024
+ "key",
4025
+ "email",
4026
+ "user",
4027
+ "salt",
4028
+ "aesKey",
4029
+ "accessToken",
4030
+ "refreshToken"
4031
+ ];
4032
+ for (const field of strings) if (typeof record[field] !== "string" || !record[field]) return null;
4033
+ if (typeof record.expiresAt !== "number") return null;
4034
+ return record;
4035
+ }
4036
+ async function writeCredentials(path, creds) {
4037
+ await mkdir(join2(path, ".."), { recursive: true });
4038
+ await writeFile(path, JSON.stringify(creds, null, 2) + "\n", { encoding: "utf8", mode: 384 });
4039
+ await chmod(path, 384);
4040
+ }
4041
+ function readSession(value) {
4042
+ if (typeof value !== "object" || value === null) return null;
4043
+ const body = value;
4044
+ const user = body.user;
4045
+ if (typeof body.access_token !== "string" || typeof body.refresh_token !== "string") return null;
4046
+ if (typeof user?.id !== "string") return null;
4047
+ const expiresAt = typeof body.expires_at === "number" ? body.expires_at : Math.floor(Date.now() / 1e3) + (typeof body.expires_in === "number" ? body.expires_in : 3600);
4048
+ return {
4049
+ accessToken: body.access_token,
4050
+ refreshToken: body.refresh_token,
4051
+ expiresAt,
4052
+ user: user.id,
4053
+ email: typeof user.email === "string" ? user.email : ""
4054
+ };
4055
+ }
4056
+ async function apiError(response, doing) {
4057
+ let detail = "";
4058
+ try {
4059
+ const body = await response.json();
4060
+ detail = String(body.msg ?? body.message ?? body.error_description ?? body.error ?? "");
4061
+ } catch {
4062
+ }
4063
+ return new Error(`${doing} failed (${response.status})${detail ? `: ${detail}` : ""}`);
4064
+ }
4065
+ async function authRequest(url, key, path, body, doing) {
4066
+ const response = await fetch(`${url}/auth/v1/${path}`, {
4067
+ method: "POST",
4068
+ headers: { apikey: key, "Content-Type": "application/json" },
4069
+ body: JSON.stringify(body)
4070
+ });
4071
+ if (!response.ok) throw await apiError(response, doing);
4072
+ return response.json();
4073
+ }
4074
+ async function requestCode(url, key, email) {
4075
+ await authRequest(url, key, "otp", { email, create_user: true }, "requesting a sign-in code");
4076
+ }
4077
+ async function verifyCode(url, key, email, token) {
4078
+ const body = await authRequest(
4079
+ url,
4080
+ key,
4081
+ "verify",
4082
+ { type: "email", email, token },
4083
+ "verifying the code"
4084
+ );
4085
+ const session = readSession(body);
4086
+ if (!session) throw new Error("the sign-in answer did not carry a session");
4087
+ return session;
4088
+ }
4089
+ async function refreshSession(url, key, refreshToken) {
4090
+ const body = await authRequest(
4091
+ url,
4092
+ key,
4093
+ "token?grant_type=refresh_token",
4094
+ { refresh_token: refreshToken },
4095
+ "refreshing the session"
4096
+ );
4097
+ const session = readSession(body);
4098
+ if (!session) throw new Error("the refresh answer did not carry a session");
4099
+ return session;
4100
+ }
4101
+ var EXPIRY_SLACK_S = 60;
4102
+ var CloudClient = class {
4103
+ constructor(creds, path) {
4104
+ this.creds = creds;
4105
+ this.path = path;
4106
+ }
4107
+ creds;
4108
+ path;
4109
+ get email() {
4110
+ return this.creds.email;
4111
+ }
4112
+ /**
4113
+ * A live access token, refreshed when near expiry. GoTrue *rotates*
4114
+ * refresh tokens, so a successful refresh is persisted immediately —
4115
+ * losing the rotation means the next launch's token is already spent
4116
+ * and the user is sent back through `login` for no crime of theirs.
4117
+ * The persistence itself is best-effort, `rememberRev`'s reason: the
4118
+ * refresh has already succeeded, and failing the user's actual request
4119
+ * over bookkeeping is the wrong way round.
4120
+ */
4121
+ async token() {
4122
+ if (this.creds.expiresAt - EXPIRY_SLACK_S > Date.now() / 1e3) return this.creds.accessToken;
4123
+ const session = await refreshSession(this.creds.url, this.creds.key, this.creds.refreshToken);
4124
+ this.creds = {
4125
+ ...this.creds,
4126
+ accessToken: session.accessToken,
4127
+ refreshToken: session.refreshToken,
4128
+ expiresAt: session.expiresAt
4129
+ };
4130
+ try {
4131
+ await writeCredentials(this.path, this.creds);
4132
+ } catch {
4133
+ }
4134
+ return this.creds.accessToken;
4135
+ }
4136
+ async rest(path, init) {
4137
+ const token = await this.token();
4138
+ return fetch(`${this.creds.url}/rest/v1/${path}`, {
4139
+ ...init,
4140
+ headers: {
4141
+ apikey: this.creds.key,
4142
+ Authorization: `Bearer ${token}`,
4143
+ "Content-Type": "application/json",
4144
+ ...init.headers ?? {}
4145
+ }
4146
+ });
4147
+ }
4148
+ /** The row, or `null` when the account has none yet. RLS scopes the select
4149
+ * to the signed-in user's own row, so there is no filter to get wrong. */
4150
+ async fetchRow() {
4151
+ const response = await this.rest("documents?select=rev,version,zone,payload", {
4152
+ method: "GET"
4153
+ });
4154
+ if (!response.ok) throw await apiError(response, "reading the cloud copy");
4155
+ const rows = await response.json();
4156
+ return readCloudRow(rows[0]);
4157
+ }
4158
+ /**
4159
+ * The compare-and-swap: `update … where rev = expected`, and the row count
4160
+ * answers whether we won. `Prefer: return=representation` is not decoration
4161
+ * — without it PostgREST returns nothing to count (the browser's `replace`
4162
+ * makes the same point with `.select()`). No insert branch on purpose: this
4163
+ * client never creates the row, so "no row" and "somebody moved it" can
4164
+ * share the miss without ambiguity — the caller re-reads either way.
4165
+ */
4166
+ async casReplace(expected, next, zone, payload) {
4167
+ const response = await this.rest(`documents?rev=eq.${expected}`, {
4168
+ method: "PATCH",
4169
+ headers: { Prefer: "return=representation" },
4170
+ body: JSON.stringify({ rev: next, version: MIRROR_VERSION, zone, payload })
4171
+ });
4172
+ if (!response.ok) throw await apiError(response, "writing the cloud copy");
4173
+ const rows = await response.json();
4174
+ return rows.length === 1;
4175
+ }
4176
+ };
4177
+ var NO_ROW = "No cloud copy exists for this account yet. Open circal in a browser, sign in and turn cloud sync on (Settings \u2192 Cloud copy), then retry.";
4178
+ var RESEALED_HERE = "The cloud copy was re-sealed under a different passphrase since this machine signed in, so the key it kept no longer opens it. Nothing was written. Run `circal-mcp login` again.";
4179
+ var UNOPENABLE = "The stored key does not open the cloud copy, though the copy claims its salt. Nothing was written. Run `circal-mcp login` again; if this repeats, the payload may be damaged.";
4180
+ function cloudStore(ctx) {
4181
+ const client = new CloudClient(ctx.credentials, ctx.path);
4182
+ const storedSalt = base64UrlToBytes(ctx.credentials.salt);
4183
+ const storedKey = base64UrlToBytes(ctx.credentials.aesKey);
4184
+ if (!storedSalt || !storedKey) {
4185
+ throw new Error(`${ctx.path} is damaged \u2014 run \`circal-mcp login\` to rewrite it.`);
4186
+ }
4187
+ const salt = storedSalt;
4188
+ const vaultReady = restoreKeyed(storedKey, salt).then(vaultOf);
4189
+ async function openRow(row) {
4190
+ const rowSalt = saltOf(row.payload);
4191
+ if (!rowSalt || rowSalt.length !== salt.length || !rowSalt.every((b, i) => b === salt[i])) {
4192
+ throw new RefusalError(RESEALED_HERE);
4193
+ }
4194
+ let text;
4195
+ try {
4196
+ text = await (await vaultReady).open(row.payload);
4197
+ } catch (error) {
4198
+ if (error instanceof WrongPassphrase) throw new RefusalError(UNOPENABLE);
4199
+ if (error instanceof UnreadablePayload)
4200
+ throw new RefusalError(
4201
+ "The cloud copy's payload is not a vault this build can read. Nothing was written."
4202
+ );
4203
+ throw error;
4204
+ }
4205
+ const contents = readMirrorFile(text);
4206
+ if (!contents) {
4207
+ throw new Error("the cloud payload decrypted, but not into a circal document");
4208
+ }
4209
+ return { ...contents, rev: row.rev, zone: row.zone || contents.zone };
4210
+ }
4211
+ async function loadOnce() {
4212
+ const row = await client.fetchRow();
4213
+ if (!row) throw new RefusalError(NO_ROW);
4214
+ const contents = await openRow(row);
4215
+ return { contents, warning: zoneWarning(contents) };
4216
+ }
4217
+ async function writeOnce(mutate) {
4218
+ let firstRev = null;
4219
+ for (let attempt = 0; attempt < 2; attempt++) {
4220
+ const row = await client.fetchRow();
4221
+ if (!row) throw new RefusalError(NO_ROW);
4222
+ const contents = await openRow(row);
4223
+ assertVersionAllowsMutation(contents);
4224
+ assertZoneAllowsMutation(contents, ctx.env);
4225
+ const { result, summary } = mutate(contents);
4226
+ const zone = row.zone || localZone();
4227
+ const next = row.rev + 1;
4228
+ const payload = await (await vaultReady).seal(serialiseMirror(result, next, zone));
4229
+ if (await client.casReplace(row.rev, next, zone, payload)) return summary;
4230
+ if (firstRev === null) firstRev = row.rev;
4231
+ else {
4232
+ throw new Error(
4233
+ `The cloud copy moved under this write: read at rev ${firstRev}, lost the swap again at rev ${row.rev}. Something else is writing it \u2014 try again.`
4234
+ );
4235
+ }
4236
+ }
4237
+ throw new Error("unreachable");
4238
+ }
4239
+ let writeQueue2 = Promise.resolve();
4240
+ return {
4241
+ load: loadOnce,
4242
+ write(mutate) {
4243
+ const run = writeQueue2.then(() => writeOnce(mutate));
4244
+ writeQueue2 = run.then(
4245
+ () => void 0,
4246
+ () => void 0
4247
+ );
4248
+ return run;
4249
+ }
4250
+ };
4251
+ }
4252
+ function ask(question, hidden = false) {
4253
+ const muted = { current: false };
4254
+ const output = new Writable({
4255
+ write(chunk, _enc, done) {
4256
+ if (!muted.current) process.stdout.write(chunk);
4257
+ done();
4258
+ }
4259
+ });
4260
+ const rl = createInterface({ input: process.stdin, output, terminal: true });
4261
+ const { promise, resolve } = Promise.withResolvers();
4262
+ rl.question(question, (answer) => {
4263
+ muted.current = false;
4264
+ if (hidden) process.stdout.write("\n");
4265
+ rl.close();
4266
+ resolve(answer.trim());
4267
+ });
4268
+ muted.current = hidden;
4269
+ return promise;
4270
+ }
4271
+ function flagValue(argv2, flag) {
4272
+ const index = argv2.indexOf(flag);
4273
+ return index >= 0 ? argv2[index + 1] : void 0;
4274
+ }
4275
+ var DEFAULT_SITE = "https://circal.day";
4276
+ function readProject(value) {
4277
+ if (typeof value !== "object" || value === null) return null;
4278
+ const body = value;
4279
+ if (typeof body.url !== "string" || !body.url) return null;
4280
+ if (typeof body.key !== "string" || !body.key) return null;
4281
+ return { url: body.url.replace(/\/+$/, ""), key: body.key };
4282
+ }
4283
+ async function resolveProject(argv2, env) {
4284
+ const url = (flagValue(argv2, "--url") || env.PUBLIC_SUPABASE_URL || "").replace(/\/+$/, "");
4285
+ const key = flagValue(argv2, "--key") || env.PUBLIC_SUPABASE_PUBLISHABLE_KEY || "";
4286
+ if (url && key) return { url, key };
4287
+ if (url || key) {
4288
+ throw new Error(
4289
+ "circal-mcp login was given half a project: --url and --key (or the two PUBLIC_SUPABASE_* variables) travel together."
4290
+ );
4291
+ }
4292
+ const site = (flagValue(argv2, "--site") || DEFAULT_SITE).replace(/\/+$/, "");
4293
+ let response;
4294
+ try {
4295
+ response = await fetch(`${site}/mcp.json`);
4296
+ } catch (error) {
4297
+ throw new Error(`could not reach ${site} to ask which project it uses`, { cause: error });
4298
+ }
4299
+ if (!response.ok) {
4300
+ throw new Error(
4301
+ `${site} did not answer /mcp.json (${response.status}) \u2014 an older deployment. Pass --url and --key instead (the publishable sb_publishable_\u2026 pair the site is built with).`
4302
+ );
4303
+ }
4304
+ const project = readProject(await response.json().catch(() => null));
4305
+ if (!project) {
4306
+ throw new Error(
4307
+ `${site} does not offer a cloud copy (its /mcp.json names no project). Pass --url and --key if you run your own.`
4308
+ );
4309
+ }
4310
+ return project;
4311
+ }
4312
+ async function runLogin(argv2, env) {
4313
+ const { url, key } = await resolveProject(argv2, env);
4314
+ const email = await ask("Email: ");
4315
+ if (!email) throw new Error("an email address is required");
4316
+ await requestCode(url, key, email);
4317
+ const code = await ask("Sign-in code (from the email): ");
4318
+ const session = await verifyCode(url, key, email, code);
4319
+ const client = new CloudClient(
4320
+ {
4321
+ url,
4322
+ key,
4323
+ email,
4324
+ user: session.user,
4325
+ salt: "",
4326
+ aesKey: "",
4327
+ accessToken: session.accessToken,
4328
+ refreshToken: session.refreshToken,
4329
+ expiresAt: session.expiresAt
4330
+ },
4331
+ credentialsPath(env)
4332
+ );
4333
+ const row = await client.fetchRow();
4334
+ if (!row) throw new Error(NO_ROW);
4335
+ const foundSalt = saltOf(row.payload);
4336
+ if (!foundSalt) throw new Error("the cloud copy's payload is not a vault this build can read");
4337
+ const salt = new Uint8Array(foundSalt);
4338
+ const passphrase = await ask("Passphrase: ", true);
4339
+ const rawKey = await deriveBits(passphrase, salt);
4340
+ try {
4341
+ await vaultOf(await restoreKeyed(rawKey, salt)).open(row.payload);
4342
+ } catch (error) {
4343
+ if (error instanceof WrongPassphrase) throw new Error(LOCKED, { cause: error });
4344
+ throw error;
4345
+ }
4346
+ const path = credentialsPath(env);
4347
+ await writeCredentials(path, {
4348
+ url,
4349
+ key,
4350
+ email: session.email || email,
4351
+ user: session.user,
4352
+ salt: bytesToBase64Url(salt),
4353
+ aesKey: bytesToBase64Url(rawKey),
4354
+ accessToken: session.accessToken,
4355
+ refreshToken: session.refreshToken,
4356
+ expiresAt: session.expiresAt
4357
+ });
4358
+ console.log(`
4359
+ Signed in as ${session.email || email}; the cloud copy opens.`);
4360
+ console.log(`Credentials written to ${path} (0600 \u2014 the derived key, never the passphrase).`);
4361
+ console.log(`
4362
+ Serve it with:
4363
+
4364
+ circal-mcp --cloud
4365
+ `);
4366
+ }
4367
+ async function runLogout(env) {
4368
+ const path = credentialsPath(env);
4369
+ await rm2(path, { force: true });
4370
+ console.log(`Removed ${path}. This machine can no longer open the cloud copy.`);
4371
+ }
4372
+
3697
4373
  // mcp/find.ts
3698
- import { readFile as readFile2, readdir, stat } from "node:fs/promises";
3699
- import { homedir } from "node:os";
3700
- import { join as join2 } from "node:path";
4374
+ import { readFile as readFile3, readdir, stat as stat2 } from "node:fs/promises";
4375
+ import { homedir as homedir2 } from "node:os";
4376
+ import { join as join3 } from "node:path";
3701
4377
  var SKIP_DIR_NAMES = { node_modules: true, Library: true, ".git": true };
3702
4378
  var MAX_FILE_BYTES = 32 * 1024 * 1024;
3703
4379
  function roots(home) {
3704
4380
  return [
3705
4381
  home,
3706
- join2(home, "Documents"),
3707
- join2(home, "Desktop"),
3708
- join2(home, "Downloads"),
3709
- join2(home, "Library", "Mobile Documents", "com~apple~CloudDocs"),
4382
+ join3(home, "Documents"),
4383
+ join3(home, "Desktop"),
4384
+ join3(home, "Downloads"),
4385
+ join3(home, "Library", "Mobile Documents", "com~apple~CloudDocs"),
3710
4386
  // iCloud Drive (Documents)
3711
- join2(home, "Dropbox"),
3712
- join2(home, "Sync")
4387
+ join3(home, "Dropbox"),
4388
+ join3(home, "Sync")
3713
4389
  ];
3714
4390
  }
3715
4391
  function appDefault(home) {
3716
- return join2(home, "Library", "Application Support", "circal", "calendar.json");
4392
+ return join3(home, "Library", "Application Support", "circal", "calendar.json");
3717
4393
  }
3718
4394
  async function jsonFilesUnder(root) {
3719
4395
  const found = [];
3720
4396
  const top = await listDir(root);
3721
4397
  for (const entry of top) {
3722
4398
  if (entry.name.startsWith(".")) continue;
3723
- const full = join2(root, entry.name);
4399
+ const full = join3(root, entry.name);
3724
4400
  if (entry.isFile()) {
3725
4401
  if (entry.name.toLowerCase().endsWith(".json")) found.push(full);
3726
4402
  continue;
@@ -3728,39 +4404,39 @@ async function jsonFilesUnder(root) {
3728
4404
  if (!entry.isDirectory() || SKIP_DIR_NAMES[entry.name]) continue;
3729
4405
  for (const sub of await listDir(full)) {
3730
4406
  if (sub.name.startsWith(".") || !sub.isFile()) continue;
3731
- if (sub.name.toLowerCase().endsWith(".json")) found.push(join2(full, sub.name));
4407
+ if (sub.name.toLowerCase().endsWith(".json")) found.push(join3(full, sub.name));
3732
4408
  }
3733
4409
  }
3734
4410
  return found;
3735
4411
  }
3736
- async function listDir(path2) {
4412
+ async function listDir(path) {
3737
4413
  try {
3738
- return await readdir(path2, { withFileTypes: true });
4414
+ return await readdir(path, { withFileTypes: true });
3739
4415
  } catch {
3740
4416
  return [];
3741
4417
  }
3742
4418
  }
3743
- async function asMirror(path2) {
4419
+ async function asMirror(path) {
3744
4420
  try {
3745
- const info = await stat(path2);
4421
+ const info = await stat2(path);
3746
4422
  if (!info.isFile() || info.size > MAX_FILE_BYTES) return null;
3747
- const text = await readFile2(path2, "utf8");
4423
+ const text = await readFile3(path, "utf8");
3748
4424
  const contents = readMirrorFile(text);
3749
- return contents ? { path: path2, contents } : null;
4425
+ return contents ? { path, contents } : null;
3750
4426
  } catch {
3751
4427
  return null;
3752
4428
  }
3753
4429
  }
3754
- async function runFind(home = homedir()) {
4430
+ async function runFind(home = homedir2()) {
3755
4431
  const searchRoots = roots(home);
3756
4432
  const candidates = /* @__PURE__ */ new Set();
3757
4433
  candidates.add(appDefault(home));
3758
4434
  for (const root of searchRoots) {
3759
- for (const path2 of await jsonFilesUnder(root)) candidates.add(path2);
4435
+ for (const path of await jsonFilesUnder(root)) candidates.add(path);
3760
4436
  }
3761
4437
  const hits = [];
3762
- for (const path2 of [...candidates].sort()) {
3763
- const hit = await asMirror(path2);
4438
+ for (const path of [...candidates].sort()) {
4439
+ const hit = await asMirror(path);
3764
4440
  if (hit) hits.push(hit);
3765
4441
  }
3766
4442
  if (hits.length === 0) {
@@ -3930,7 +4606,8 @@ function resolveSpanForCreate(input, day, defaultDuration) {
3930
4606
  const at = atMinutes(day, parseTime(input.start, "start")).getTime();
3931
4607
  return { start: at, end: at, allDay: false, task: true };
3932
4608
  }
3933
- const noTimeGiven = input.start === void 0 && input.end === void 0 && input.duration === void 0;
4609
+ const endIsDay = input.end !== void 0 && DATE_KEY_RE.test(input.end);
4610
+ const noTimeGiven = input.start === void 0 && (input.end === void 0 || endIsDay) && input.duration === void 0;
3934
4611
  const allDay = input.allDay === true || input.allDay === void 0 && noTimeGiven;
3935
4612
  if (allDay) {
3936
4613
  const days = input.end ? Math.max(1, daysBetween(day, fromDayKey(input.end)) + 1) : 1;
@@ -4024,7 +4701,11 @@ function buildEventRecord(input, calendars, settings) {
4024
4701
  // assembled would be a list of people who were never asked.
4025
4702
  organizer: null,
4026
4703
  attendees: [],
4027
- attendeeCount: 0
4704
+ attendeeCount: 0,
4705
+ // The stamp is the caller's: `performCreateEvent` and the quick-add path
4706
+ // are where this record starts existing, the same rule `draftEvent`
4707
+ // follows in the app.
4708
+ updatedAt: 0
4028
4709
  });
4029
4710
  }
4030
4711
  function applyPatchToEvent(base, input, calendars, settings, anchorMs = base.start) {
@@ -4064,9 +4745,21 @@ function applyPatchToEvent(base, input, calendars, settings, anchorMs = base.sta
4064
4745
  // had asked and who was coming.
4065
4746
  organizer: base.organizer,
4066
4747
  attendees: base.attendees,
4067
- attendeeCount: base.attendeeCount
4748
+ attendeeCount: base.attendeeCount,
4749
+ // Carried, and overwritten by the reducer that commits the patch: it is
4750
+ // the base's stamp until this edit lands, never a fresh record's zero.
4751
+ updatedAt: base.updatedAt
4068
4752
  });
4069
4753
  }
4754
+ function carried(contents) {
4755
+ return {
4756
+ events: contents.events,
4757
+ calendars: contents.calendars,
4758
+ settings: contents.settings,
4759
+ settingsAt: contents.settingsAt,
4760
+ graves: contents.graves
4761
+ };
4762
+ }
4070
4763
  function calendarName(calendars, id) {
4071
4764
  return calendars.find((c) => c.id === id)?.name ?? "\u2014";
4072
4765
  }
@@ -4200,25 +4893,17 @@ function performQuickAdd(contents, text, date) {
4200
4893
  title: parsed.title,
4201
4894
  ...parsed.repeat
4202
4895
  });
4203
- const event = { ...record, id: newId() };
4896
+ const event = { ...record, id: newId(), updatedAt: Date.now() };
4204
4897
  return {
4205
- mutated: {
4206
- events: [...contents.events, event],
4207
- calendars: contents.calendars,
4208
- settings: contents.settings
4209
- },
4898
+ mutated: { ...carried(contents), events: [...contents.events, event] },
4210
4899
  message: `Created ${eventSummaryLine(event, contents.calendars, contents.settings.clock)}`
4211
4900
  };
4212
4901
  }
4213
4902
  function performCreateEvent(contents, input) {
4214
4903
  const record = buildEventRecord(input, contents.calendars, contents.settings);
4215
- const event = { ...record, id: newId() };
4904
+ const event = { ...record, id: newId(), updatedAt: Date.now() };
4216
4905
  return {
4217
- mutated: {
4218
- events: [...contents.events, event],
4219
- calendars: contents.calendars,
4220
- settings: contents.settings
4221
- },
4906
+ mutated: { ...carried(contents), events: [...contents.events, event] },
4222
4907
  message: `Created ${eventSummaryLine(event, contents.calendars, contents.settings.clock)}`
4223
4908
  };
4224
4909
  }
@@ -4230,10 +4915,10 @@ function performUpdateEvent(contents, id, scopeArg, occurrence, patch) {
4230
4915
  const clock = contents.settings.clock;
4231
4916
  if (scope === "all") {
4232
4917
  const record2 = applyPatchToEvent(original, patch, contents.calendars, contents.settings);
4233
- const events = applyUpdate(contents.events, original.id, record2);
4918
+ const events = applyUpdate(contents.events, original.id, record2, Date.now());
4234
4919
  const updated = events.find((e) => e.id === original.id);
4235
4920
  return {
4236
- mutated: { events, calendars: contents.calendars, settings: contents.settings },
4921
+ mutated: { ...carried(contents), events },
4237
4922
  message: `Updated ${eventSummaryLine(updated, contents.calendars, clock)}`
4238
4923
  };
4239
4924
  }
@@ -4245,22 +4930,22 @@ function performUpdateEvent(contents, id, scopeArg, occurrence, patch) {
4245
4930
  ...NEVER,
4246
4931
  seriesId: original.id,
4247
4932
  seriesKey: instantKey(at),
4248
- id: newId()
4933
+ id: newId(),
4934
+ // An agent's write is a mutation like the app's, so it stamps like one:
4935
+ // unstamped, the browser's own copy of this document wins on the next
4936
+ // merge and the agent's work is quietly dropped.
4937
+ updatedAt: Date.now()
4249
4938
  };
4250
4939
  return {
4251
- mutated: {
4252
- events: [...contents.events, override],
4253
- calendars: contents.calendars,
4254
- settings: contents.settings
4255
- },
4940
+ mutated: { ...carried(contents), events: [...contents.events, override] },
4256
4941
  message: `Updated one occurrence: ${eventSummaryLine(override, contents.calendars, clock)}`
4257
4942
  };
4258
4943
  }
4259
4944
  const record = applyPatchToEvent(original, patch, contents.calendars, contents.settings, at);
4260
- const split = applySplit(contents.events, original.id, at, record, newId);
4945
+ const split = applySplit(contents.events, original.id, at, record, newId, Date.now());
4261
4946
  if (!split.created) throw new RefusalError(`no event with id "${id}"`);
4262
4947
  return {
4263
- mutated: { events: split.events, calendars: contents.calendars, settings: contents.settings },
4948
+ mutated: { ...carried(contents), events: split.events },
4264
4949
  message: `Updated from ${dayKey(new Date(at))} on: ${eventSummaryLine(split.created, contents.calendars, clock)}`
4265
4950
  };
4266
4951
  }
@@ -4271,30 +4956,38 @@ function performDeleteEvent(contents, id, scopeArg, occurrence) {
4271
4956
  const scope = requireScope(original, scopeArg);
4272
4957
  const title = original.title || "Untitled";
4273
4958
  if (scope === "all") {
4959
+ const removed = applyRemove(contents.events, original.id, Date.now());
4274
4960
  return {
4961
+ // The graves ride with the removal, because an agent's deletion has to
4962
+ // survive the browser's next merge: without them the tab still holding
4963
+ // the event wins on stamps and writes it straight back.
4275
4964
  mutated: {
4276
- events: applyRemove(contents.events, original.id),
4277
- calendars: contents.calendars,
4278
- settings: contents.settings
4965
+ ...carried(contents),
4966
+ events: removed.events,
4967
+ graves: [...contents.graves, ...removed.graves]
4279
4968
  },
4280
4969
  message: `Deleted "${title}" (the whole series).`
4281
4970
  };
4282
4971
  }
4283
4972
  const at = resolveOccurrenceRequired(original, occurrence, scope);
4284
4973
  if (scope === "one") {
4285
- const events = applyUpdate(contents.events, original.id, {
4286
- exdates: [...original.exdates, instantKey(at)]
4287
- });
4974
+ const events = applyUpdate(
4975
+ contents.events,
4976
+ original.id,
4977
+ { exdates: [...original.exdates, instantKey(at)] },
4978
+ Date.now()
4979
+ );
4288
4980
  return {
4289
- mutated: { events, calendars: contents.calendars, settings: contents.settings },
4981
+ mutated: { ...carried(contents), events },
4290
4982
  message: `Deleted "${title}" on ${dayKey(new Date(at))} only \u2014 the series continues.`
4291
4983
  };
4292
4984
  }
4985
+ const cut = applyTruncate(contents.events, original.id, at, Date.now());
4293
4986
  return {
4294
4987
  mutated: {
4295
- events: applyTruncate(contents.events, original.id, at),
4296
- calendars: contents.calendars,
4297
- settings: contents.settings
4988
+ ...carried(contents),
4989
+ events: cut.events,
4990
+ graves: [...contents.graves, ...cut.graves]
4298
4991
  },
4299
4992
  message: `Deleted "${title}" from ${dayKey(new Date(at))} on.`
4300
4993
  };
@@ -4306,9 +4999,9 @@ function performToggleDone(contents, id, occurrence) {
4306
4999
  refuseIfFeed(original, contents.calendars);
4307
5000
  const at = resolveInstant(original, occurrence);
4308
5001
  const wasDone = original.doneKeys.includes(instantKey(at));
4309
- const events = applyToggleDone(contents.events, original.id, at);
5002
+ const events = applyToggleDone(contents.events, original.id, at, Date.now());
4310
5003
  return {
4311
- mutated: { events, calendars: contents.calendars, settings: contents.settings },
5004
+ mutated: { ...carried(contents), events },
4312
5005
  message: `${wasDone ? "Reopened" : "Completed"} "${original.title || "Untitled"}" on ${dayKey(new Date(at))}.`
4313
5006
  };
4314
5007
  }
@@ -4320,15 +5013,12 @@ function performCreateCalendar(contents, name, color, icon) {
4320
5013
  name: trimmed,
4321
5014
  color: color || PALETTE[contents.calendars.length * 3 % PALETTE.length],
4322
5015
  icon: icon ?? "",
4323
- visible: true
5016
+ visible: true,
5017
+ updatedAt: Date.now()
4324
5018
  });
4325
5019
  if (!calendar) throw new RefusalError("the assembled calendar failed validation");
4326
5020
  return {
4327
- mutated: {
4328
- events: contents.events,
4329
- calendars: [...contents.calendars, calendar],
4330
- settings: contents.settings
4331
- },
5021
+ mutated: { ...carried(contents), calendars: [...contents.calendars, calendar] },
4332
5022
  message: `Created calendar "${calendar.name}" \u2014 id=${calendar.id}`
4333
5023
  };
4334
5024
  }
@@ -4416,7 +5106,7 @@ var SCOPE_DESCRIPTION = 'Required when the target event repeats: "one" (just thi
4416
5106
  var OCCURRENCE_DESCRIPTION = 'Which occurrence, as a YYYY-MM-DD date or a full "YYYY-MM-DDTHH:mm:ss" instant. Required with scope "one" or "future".';
4417
5107
  var scopeSchema = z.enum(["one", "future", "all"]).describe(SCOPE_DESCRIPTION);
4418
5108
  var occurrenceSchema = z.string().describe(OCCURRENCE_DESCRIPTION);
4419
- function registerTools(server, ctx2) {
5109
+ function registerTools(server, store3) {
4420
5110
  server.registerTool(
4421
5111
  "list_day",
4422
5112
  {
@@ -4425,7 +5115,7 @@ function registerTools(server, ctx2) {
4425
5115
  annotations: { readOnlyHint: true }
4426
5116
  },
4427
5117
  async ({ date }) => guarded(async () => {
4428
- const { contents, warning } = await loadForRead(ctx2);
5118
+ const { contents, warning } = await store3.load();
4429
5119
  return withWarning(listDay(contents, date), warning);
4430
5120
  })
4431
5121
  );
@@ -4440,7 +5130,7 @@ function registerTools(server, ctx2) {
4440
5130
  annotations: { readOnlyHint: true }
4441
5131
  },
4442
5132
  async ({ from, to }) => guarded(async () => {
4443
- const { contents, warning } = await loadForRead(ctx2);
5133
+ const { contents, warning } = await store3.load();
4444
5134
  return withWarning(listRange(contents, from, to), warning);
4445
5135
  })
4446
5136
  );
@@ -4455,7 +5145,7 @@ function registerTools(server, ctx2) {
4455
5145
  annotations: { readOnlyHint: true }
4456
5146
  },
4457
5147
  async ({ query, limit }) => guarded(async () => {
4458
- const { contents, warning } = await loadForRead(ctx2);
5148
+ const { contents, warning } = await store3.load();
4459
5149
  return withWarning(searchEvents(contents, query, limit), warning);
4460
5150
  })
4461
5151
  );
@@ -4472,7 +5162,7 @@ function registerTools(server, ctx2) {
4472
5162
  annotations: { readOnlyHint: true }
4473
5163
  },
4474
5164
  async ({ date, minutes, from, to }) => guarded(async () => {
4475
- const { contents, warning } = await loadForRead(ctx2);
5165
+ const { contents, warning } = await store3.load();
4476
5166
  return withWarning(freeSlots(contents, date, minutes, from, to), warning);
4477
5167
  })
4478
5168
  );
@@ -4484,7 +5174,7 @@ function registerTools(server, ctx2) {
4484
5174
  annotations: { readOnlyHint: true }
4485
5175
  },
4486
5176
  async () => guarded(async () => {
4487
- const { contents, warning } = await loadForRead(ctx2);
5177
+ const { contents, warning } = await store3.load();
4488
5178
  return withWarning(listCalendars(contents), warning);
4489
5179
  })
4490
5180
  );
@@ -4496,7 +5186,7 @@ function registerTools(server, ctx2) {
4496
5186
  annotations: { readOnlyHint: true }
4497
5187
  },
4498
5188
  async ({ id }) => guarded(async () => {
4499
- const { contents, warning } = await loadForRead(ctx2);
5189
+ const { contents, warning } = await store3.load();
4500
5190
  return withWarning(describeEvent(contents, id), warning);
4501
5191
  })
4502
5192
  );
@@ -4513,7 +5203,7 @@ function registerTools(server, ctx2) {
4513
5203
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
4514
5204
  },
4515
5205
  async ({ text, date }) => guarded(
4516
- () => casWrite(ctx2, (contents) => {
5206
+ () => store3.write((contents) => {
4517
5207
  const outcome = performQuickAdd(contents, text, date);
4518
5208
  return { result: outcome.mutated, summary: outcome.message };
4519
5209
  })
@@ -4527,7 +5217,7 @@ function registerTools(server, ctx2) {
4527
5217
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
4528
5218
  },
4529
5219
  async (input) => guarded(
4530
- () => casWrite(ctx2, (contents) => {
5220
+ () => store3.write((contents) => {
4531
5221
  const outcome = performCreateEvent(contents, input);
4532
5222
  return { result: outcome.mutated, summary: outcome.message };
4533
5223
  })
@@ -4546,7 +5236,7 @@ function registerTools(server, ctx2) {
4546
5236
  annotations: { destructiveHint: true, idempotentHint: false }
4547
5237
  },
4548
5238
  async ({ id, scope, occurrence, patch }) => guarded(
4549
- () => casWrite(ctx2, (contents) => {
5239
+ () => store3.write((contents) => {
4550
5240
  const outcome = performUpdateEvent(contents, id, scope, occurrence, patch);
4551
5241
  return { result: outcome.mutated, summary: outcome.message };
4552
5242
  })
@@ -4564,7 +5254,7 @@ function registerTools(server, ctx2) {
4564
5254
  annotations: { destructiveHint: true, idempotentHint: false }
4565
5255
  },
4566
5256
  async ({ id, scope, occurrence }) => guarded(
4567
- () => casWrite(ctx2, (contents) => {
5257
+ () => store3.write((contents) => {
4568
5258
  const outcome = performDeleteEvent(contents, id, scope, occurrence);
4569
5259
  return { result: outcome.mutated, summary: outcome.message };
4570
5260
  })
@@ -4581,7 +5271,7 @@ function registerTools(server, ctx2) {
4581
5271
  annotations: { destructiveHint: false, idempotentHint: false }
4582
5272
  },
4583
5273
  async ({ id, occurrence }) => guarded(
4584
- () => casWrite(ctx2, (contents) => {
5274
+ () => store3.write((contents) => {
4585
5275
  const outcome = performToggleDone(contents, id, occurrence);
4586
5276
  return { result: outcome.mutated, summary: outcome.message };
4587
5277
  })
@@ -4599,7 +5289,7 @@ function registerTools(server, ctx2) {
4599
5289
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
4600
5290
  },
4601
5291
  async ({ name, color, icon }) => guarded(
4602
- () => casWrite(ctx2, (contents) => {
5292
+ () => store3.write((contents) => {
4603
5293
  const outcome = performCreateCalendar(contents, name, color, icon);
4604
5294
  return { result: outcome.mutated, summary: outcome.message };
4605
5295
  })
@@ -4608,10 +5298,18 @@ function registerTools(server, ctx2) {
4608
5298
  }
4609
5299
 
4610
5300
  // mcp/index.ts
4611
- var HELP = `circal-mcp \u2014 reads and writes circal's mirror file over stdio (MCP)
5301
+ var HELP = `circal-mcp \u2014 reads and writes a circal calendar over stdio (MCP)
4612
5302
 
4613
5303
  Usage:
4614
5304
  circal-mcp --file <path> Start the server against a mirror file
5305
+ circal-mcp --cloud Start the server against the cloud copy
5306
+ (sign in first with \`circal-mcp login\`)
5307
+ circal-mcp login Sign in to the cloud copy and remember the
5308
+ derived key on this machine (interactive:
5309
+ email, emailed code, passphrase; asks circal.day
5310
+ which project it uses \u2014 --site <url> for a fork,
5311
+ or --url and --key to name one outright)
5312
+ circal-mcp logout Forget the remembered key and session
4615
5313
  circal-mcp --find Search common locations for a mirror file and
4616
5314
  print a ready-to-paste client config (read-only,
4617
5315
  does not start the server)
@@ -4619,8 +5317,10 @@ Usage:
4619
5317
 
4620
5318
  Environment:
4621
5319
  CIRCAL_FILE Mirror file path, same as --file
5320
+ CIRCAL_CLOUD_CREDENTIALS Where \`login\` keeps its record (default:
5321
+ the platform config dir, circal/cloud.json)
4622
5322
  CIRCAL_ALLOW_ZONE_MISMATCH Set to 1 to allow writes when this process's
4623
- zone does not match the file's recorded zone
5323
+ zone does not match the document's recorded zone
4624
5324
  `;
4625
5325
  var argv = process.argv.slice(2);
4626
5326
  if (argv.includes("--help") || argv.includes("-h")) {
@@ -4631,18 +5331,41 @@ if (argv.includes("--find")) {
4631
5331
  await runFind();
4632
5332
  process.exit(0);
4633
5333
  }
4634
- var path;
4635
- try {
4636
- path = resolveFilePath(argv, process.env);
4637
- } catch (error) {
4638
- const message = error instanceof Error ? error.message : String(error);
4639
- console.error(`${message} Run \`circal-mcp --find\` to search common locations for one.`);
4640
- process.exit(1);
5334
+ if (argv[0] === "login" || argv[0] === "logout") {
5335
+ try {
5336
+ if (argv[0] === "login") await runLogin(argv, process.env);
5337
+ else await runLogout(process.env);
5338
+ process.exit(0);
5339
+ } catch (error) {
5340
+ console.error(error instanceof Error ? error.message : String(error));
5341
+ process.exit(1);
5342
+ }
5343
+ }
5344
+ var store2;
5345
+ if (argv.includes("--cloud")) {
5346
+ const path = credentialsPath(process.env);
5347
+ const credentials = await readCredentials(path);
5348
+ if (!credentials) {
5349
+ console.error(
5350
+ `circal-mcp --cloud needs a sign-in and found none at ${path}. Run \`circal-mcp login\` first.`
5351
+ );
5352
+ process.exit(1);
5353
+ }
5354
+ store2 = cloudStore({ credentials, path, env: process.env });
5355
+ } else {
5356
+ let path;
5357
+ try {
5358
+ path = resolveFilePath(argv, process.env);
5359
+ } catch (error) {
5360
+ const message = error instanceof Error ? error.message : String(error);
5361
+ console.error(`${message} Run \`circal-mcp --find\` to search common locations for one.`);
5362
+ process.exit(1);
5363
+ }
5364
+ store2 = fileStore({ path, env: process.env });
4641
5365
  }
4642
- var ctx = { path, env: process.env };
4643
5366
  function createServer() {
4644
5367
  const server = new McpServer({ name: "circal", version: "1.0.0" });
4645
- registerTools(server, ctx);
5368
+ registerTools(server, store2);
4646
5369
  return server;
4647
5370
  }
4648
5371
  void serveStdio(createServer);