circal-mcp 0.2.1 → 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 +1372 -694
  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,11 +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, stat } from "node:fs/promises";
1464
- import { dirname, join } from "node:path";
1465
- import { setTimeout as sleep } from "node:timers/promises";
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
+ }
1466
1555
 
1467
1556
  // src/lib/time.ts
1468
1557
  var MINUTES_PER_DAY = 1440;
@@ -2097,70 +2186,6 @@ function splitRule(rule, seedMs, at) {
2097
2186
  };
2098
2187
  }
2099
2188
 
2100
- // src/lib/fuzzy.ts
2101
- var MATCH = 10;
2102
- var RUN = 9;
2103
- var WORD_START = 12;
2104
- var LEAD = 6;
2105
- var GAP = 1;
2106
- var GAP_CAP = 12;
2107
- var LENGTH = 0.2;
2108
- var TYPO = 40;
2109
- var TYPO_MIN = 4;
2110
- var WORD_BREAK = /[\s\-_/:.,(–—]/;
2111
- function scan(text, query, start = 0) {
2112
- let score = 0;
2113
- let at = start;
2114
- let previous = -2;
2115
- let run = 0;
2116
- for (const character of query) {
2117
- if (character === " ") continue;
2118
- const found = text.indexOf(character, at);
2119
- if (found === -1) return null;
2120
- score += MATCH;
2121
- if (found === previous + 1) score += RUN * ++run;
2122
- else {
2123
- run = 0;
2124
- score -= Math.min(found - at, GAP_CAP) * GAP;
2125
- }
2126
- if (found === 0) score += WORD_START + LEAD;
2127
- else if (WORD_BREAK.test(text[found - 1])) score += WORD_START;
2128
- previous = found;
2129
- at = found + 1;
2130
- }
2131
- return score - text.length * LENGTH;
2132
- }
2133
- function bestScan(text, query) {
2134
- const first = query.replace(/\s+/g, "")[0];
2135
- if (!first) return null;
2136
- let best = null;
2137
- let tried = false;
2138
- for (let i = text.indexOf(first); i !== -1; i = text.indexOf(first, i + 1)) {
2139
- if (tried && i !== 0 && !WORD_BREAK.test(text[i - 1])) continue;
2140
- tried = true;
2141
- const score = scan(text, query, i);
2142
- if (score !== null && (best === null || score > best)) best = score;
2143
- }
2144
- return best;
2145
- }
2146
- function fuzzyScore(text, query) {
2147
- const wanted = query.trim().toLowerCase();
2148
- if (!wanted) return 0;
2149
- if (!text) return null;
2150
- const haystack = text.toLowerCase();
2151
- const clean = bestScan(haystack, wanted);
2152
- if (clean !== null) return clean;
2153
- const letters = wanted.replace(/\s+/g, "");
2154
- if (letters.length < TYPO_MIN) return null;
2155
- let best = null;
2156
- for (let i = 0; i < wanted.length; i++) {
2157
- if (wanted[i] === " ") continue;
2158
- const score = bestScan(haystack, wanted.slice(0, i) + wanted.slice(i + 1));
2159
- if (score !== null && (best === null || score > best)) best = score;
2160
- }
2161
- return best === null ? null : best - TYPO;
2162
- }
2163
-
2164
2189
  // src/lib/events.ts
2165
2190
  var ICON_MAX = 16;
2166
2191
  var ICON_NAME_MAX = 48;
@@ -2235,6 +2260,25 @@ function readGuests(list, count) {
2235
2260
  attendeeCount: Math.min(Math.max(claimed, people.length), ATTENDEE_COUNT_MAX)
2236
2261
  };
2237
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
+ }
2238
2282
  function overridesIn(events) {
2239
2283
  const out = /* @__PURE__ */ new Map();
2240
2284
  for (const event of events) {
@@ -2377,505 +2421,64 @@ function spanLabel(occ) {
2377
2421
  return occ.dayCount > 1 ? `Day ${occ.dayIndex}/${occ.dayCount}` : null;
2378
2422
  }
2379
2423
 
2380
- // src/lib/ics.ts
2381
- var encoder = new TextEncoder();
2382
- function fnv1a(source) {
2383
- let h = 2166136261;
2384
- for (let i = 0; i < source.length; i++) {
2385
- h ^= source.charCodeAt(i);
2386
- h = Math.imul(h, 16777619) >>> 0;
2387
- }
2388
- return h.toString(36);
2389
- }
2390
-
2391
- // src/lib/places.ts
2392
- var COORDS = {
2393
- "Africa/Abidjan": [5.32, -4.03],
2394
- "Africa/Accra": [5.55, -0.22],
2395
- "Africa/Addis_Ababa": [9.03, 38.7],
2396
- "Africa/Algiers": [36.78, 3.05],
2397
- "Africa/Asmara": [15.33, 38.88],
2398
- "Africa/Bamako": [12.65, -8],
2399
- "Africa/Bangui": [4.37, 18.58],
2400
- "Africa/Banjul": [13.47, -16.65],
2401
- "Africa/Bissau": [11.85, -15.58],
2402
- "Africa/Blantyre": [-15.78, 35],
2403
- "Africa/Brazzaville": [-4.27, 15.28],
2404
- "Africa/Bujumbura": [-3.38, 29.37],
2405
- "Africa/Cairo": [30.05, 31.25],
2406
- "Africa/Casablanca": [33.65, -7.58],
2407
- "Africa/Ceuta": [35.88, -5.32],
2408
- "Africa/Conakry": [9.52, -13.72],
2409
- "Africa/Dakar": [14.67, -17.43],
2410
- "Africa/Dar_es_Salaam": [-6.8, 39.28],
2411
- "Africa/Djibouti": [11.6, 43.15],
2412
- "Africa/Douala": [4.05, 9.7],
2413
- "Africa/El_Aaiun": [27.15, -13.2],
2414
- "Africa/Freetown": [8.5, -13.25],
2415
- "Africa/Gaborone": [-24.65, 25.92],
2416
- "Africa/Harare": [-17.83, 31.05],
2417
- "Africa/Johannesburg": [-26.25, 28],
2418
- "Africa/Juba": [4.85, 31.62],
2419
- "Africa/Kampala": [0.32, 32.42],
2420
- "Africa/Khartoum": [15.6, 32.53],
2421
- "Africa/Kigali": [-1.95, 30.07],
2422
- "Africa/Kinshasa": [-4.3, 15.3],
2423
- "Africa/Lagos": [6.45, 3.4],
2424
- "Africa/Libreville": [0.38, 9.45],
2425
- "Africa/Lome": [6.13, 1.22],
2426
- "Africa/Luanda": [-8.8, 13.23],
2427
- "Africa/Lubumbashi": [-11.67, 27.47],
2428
- "Africa/Lusaka": [-15.42, 28.28],
2429
- "Africa/Malabo": [3.75, 8.78],
2430
- "Africa/Maputo": [-25.97, 32.58],
2431
- "Africa/Maseru": [-29.47, 27.5],
2432
- "Africa/Mbabane": [-26.3, 31.1],
2433
- "Africa/Mogadishu": [2.07, 45.37],
2434
- "Africa/Monrovia": [6.3, -10.78],
2435
- "Africa/Nairobi": [-1.28, 36.82],
2436
- "Africa/Ndjamena": [12.12, 15.05],
2437
- "Africa/Niamey": [13.52, 2.12],
2438
- "Africa/Nouakchott": [18.1, -15.95],
2439
- "Africa/Ouagadougou": [12.37, -1.52],
2440
- "Africa/Porto-Novo": [6.48, 2.62],
2441
- "Africa/Sao_Tome": [0.33, 6.73],
2442
- "Africa/Tripoli": [32.9, 13.18],
2443
- "Africa/Tunis": [36.8, 10.18],
2444
- "Africa/Windhoek": [-22.57, 17.1],
2445
- "America/Adak": [51.88, -176.66],
2446
- "America/Anchorage": [61.22, -149.9],
2447
- "America/Anguilla": [18.2, -63.07],
2448
- "America/Antigua": [17.05, -61.8],
2449
- "America/Araguaina": [-7.2, -48.2],
2450
- "America/Argentina/Buenos_Aires": [-34.6, -58.45],
2451
- "America/Argentina/Catamarca": [-28.47, -65.78],
2452
- "America/Argentina/Cordoba": [-31.4, -64.18],
2453
- "America/Argentina/Jujuy": [-24.18, -65.3],
2454
- "America/Argentina/La_Rioja": [-29.43, -66.85],
2455
- "America/Argentina/Mendoza": [-32.88, -68.82],
2456
- "America/Argentina/Rio_Gallegos": [-51.63, -69.22],
2457
- "America/Argentina/Salta": [-24.78, -65.42],
2458
- "America/Argentina/San_Juan": [-31.53, -68.52],
2459
- "America/Argentina/San_Luis": [-33.32, -66.35],
2460
- "America/Argentina/Tucuman": [-26.82, -65.22],
2461
- "America/Argentina/Ushuaia": [-54.8, -68.3],
2462
- "America/Aruba": [12.5, -69.97],
2463
- "America/Asuncion": [-25.27, -57.67],
2464
- "America/Atikokan": [48.76, -91.62],
2465
- "America/Bahia": [-12.98, -38.52],
2466
- "America/Bahia_Banderas": [20.8, -105.25],
2467
- "America/Barbados": [13.1, -59.62],
2468
- "America/Belem": [-1.45, -48.48],
2469
- "America/Belize": [17.5, -88.2],
2470
- "America/Blanc-Sablon": [51.42, -57.12],
2471
- "America/Boa_Vista": [2.82, -60.67],
2472
- "America/Bogota": [4.6, -74.08],
2473
- "America/Boise": [43.61, -116.2],
2474
- "America/Cambridge_Bay": [69.11, -105.05],
2475
- "America/Campo_Grande": [-20.45, -54.62],
2476
- "America/Cancun": [21.08, -86.77],
2477
- "America/Caracas": [10.5, -66.93],
2478
- "America/Cayenne": [4.93, -52.33],
2479
- "America/Cayman": [19.3, -81.38],
2480
- "America/Chicago": [41.85, -87.65],
2481
- "America/Chihuahua": [28.63, -106.08],
2482
- "America/Ciudad_Juarez": [31.73, -106.48],
2483
- "America/Costa_Rica": [9.93, -84.08],
2484
- "America/Coyhaique": [-45.57, -72.07],
2485
- "America/Creston": [49.1, -116.52],
2486
- "America/Cuiaba": [-15.58, -56.08],
2487
- "America/Curacao": [12.18, -69],
2488
- "America/Danmarkshavn": [76.77, -18.67],
2489
- "America/Dawson": [64.07, -139.42],
2490
- "America/Dawson_Creek": [55.77, -120.23],
2491
- "America/Denver": [39.74, -104.98],
2492
- "America/Detroit": [42.33, -83.05],
2493
- "America/Dominica": [15.3, -61.4],
2494
- "America/Edmonton": [53.55, -113.47],
2495
- "America/Eirunepe": [-6.67, -69.87],
2496
- "America/El_Salvador": [13.7, -89.2],
2497
- "America/Fort_Nelson": [58.8, -122.7],
2498
- "America/Fortaleza": [-3.72, -38.5],
2499
- "America/Glace_Bay": [46.2, -59.95],
2500
- "America/Goose_Bay": [53.33, -60.42],
2501
- "America/Grand_Turk": [21.47, -71.13],
2502
- "America/Grenada": [12.05, -61.75],
2503
- "America/Guadeloupe": [16.23, -61.53],
2504
- "America/Guatemala": [14.63, -90.52],
2505
- "America/Guayaquil": [-2.17, -79.83],
2506
- "America/Guyana": [6.8, -58.17],
2507
- "America/Halifax": [44.65, -63.6],
2508
- "America/Havana": [23.13, -82.37],
2509
- "America/Hermosillo": [29.07, -110.97],
2510
- "America/Indiana/Indianapolis": [39.77, -86.16],
2511
- "America/Indiana/Knox": [41.3, -86.62],
2512
- "America/Indiana/Marengo": [38.38, -86.34],
2513
- "America/Indiana/Petersburg": [38.49, -87.28],
2514
- "America/Indiana/Tell_City": [37.95, -86.76],
2515
- "America/Indiana/Vevay": [38.75, -85.07],
2516
- "America/Indiana/Vincennes": [38.68, -87.53],
2517
- "America/Indiana/Winamac": [41.05, -86.6],
2518
- "America/Inuvik": [68.35, -133.72],
2519
- "America/Iqaluit": [63.73, -68.47],
2520
- "America/Jamaica": [17.97, -76.79],
2521
- "America/Juneau": [58.3, -134.42],
2522
- "America/Kentucky/Louisville": [38.25, -85.76],
2523
- "America/Kentucky/Monticello": [36.83, -84.85],
2524
- "America/Kralendijk": [12.15, -68.28],
2525
- "America/La_Paz": [-16.5, -68.15],
2526
- "America/Lima": [-12.05, -77.05],
2527
- "America/Los_Angeles": [34.05, -118.24],
2528
- "America/Lower_Princes": [18.05, -63.05],
2529
- "America/Maceio": [-9.67, -35.72],
2530
- "America/Managua": [12.15, -86.28],
2531
- "America/Manaus": [-3.13, -60.02],
2532
- "America/Marigot": [18.07, -63.08],
2533
- "America/Martinique": [14.6, -61.08],
2534
- "America/Matamoros": [25.83, -97.5],
2535
- "America/Mazatlan": [23.22, -106.42],
2536
- "America/Menominee": [45.11, -87.61],
2537
- "America/Merida": [20.97, -89.62],
2538
- "America/Metlakatla": [55.13, -131.58],
2539
- "America/Mexico_City": [19.4, -99.15],
2540
- "America/Miquelon": [47.05, -56.33],
2541
- "America/Moncton": [46.1, -64.78],
2542
- "America/Monterrey": [25.67, -100.32],
2543
- "America/Montevideo": [-34.91, -56.21],
2544
- "America/Montserrat": [16.72, -62.22],
2545
- "America/Nassau": [25.08, -77.35],
2546
- "America/New_York": [40.71, -74.01],
2547
- "America/Nome": [64.5, -165.41],
2548
- "America/Noronha": [-3.85, -32.42],
2549
- "America/North_Dakota/Beulah": [47.26, -101.78],
2550
- "America/North_Dakota/Center": [47.12, -101.3],
2551
- "America/North_Dakota/New_Salem": [46.84, -101.41],
2552
- "America/Nuuk": [64.18, -51.73],
2553
- "America/Ojinaga": [29.57, -104.42],
2554
- "America/Panama": [8.97, -79.53],
2555
- "America/Paramaribo": [5.83, -55.17],
2556
- "America/Phoenix": [33.45, -112.07],
2557
- "America/Port-au-Prince": [18.53, -72.33],
2558
- "America/Port_of_Spain": [10.65, -61.52],
2559
- "America/Porto_Velho": [-8.77, -63.9],
2560
- "America/Puerto_Rico": [18.47, -66.11],
2561
- "America/Punta_Arenas": [-53.15, -70.92],
2562
- "America/Rankin_Inlet": [62.82, -92.08],
2563
- "America/Recife": [-8.05, -34.9],
2564
- "America/Regina": [50.4, -104.65],
2565
- "America/Resolute": [74.7, -94.83],
2566
- "America/Rio_Branco": [-9.97, -67.8],
2567
- "America/Santarem": [-2.43, -54.87],
2568
- "America/Santiago": [-33.45, -70.67],
2569
- "America/Santo_Domingo": [18.47, -69.9],
2570
- "America/Sao_Paulo": [-23.53, -46.62],
2571
- "America/Scoresbysund": [70.48, -21.97],
2572
- "America/Sitka": [57.18, -135.3],
2573
- "America/St_Barthelemy": [17.88, -62.85],
2574
- "America/St_Johns": [47.57, -52.72],
2575
- "America/St_Kitts": [17.3, -62.72],
2576
- "America/St_Lucia": [14.02, -61],
2577
- "America/St_Thomas": [18.35, -64.93],
2578
- "America/St_Vincent": [13.15, -61.23],
2579
- "America/Swift_Current": [50.28, -107.83],
2580
- "America/Tegucigalpa": [14.1, -87.22],
2581
- "America/Thule": [76.57, -68.78],
2582
- "America/Tijuana": [32.53, -117.02],
2583
- "America/Toronto": [43.65, -79.38],
2584
- "America/Tortola": [18.45, -64.62],
2585
- "America/Vancouver": [49.27, -123.12],
2586
- "America/Whitehorse": [60.72, -135.05],
2587
- "America/Winnipeg": [49.88, -97.15],
2588
- "America/Yakutat": [59.55, -139.73],
2589
- "Antarctica/Casey": [-66.28, 110.52],
2590
- "Antarctica/Davis": [-68.58, 77.97],
2591
- "Antarctica/DumontDUrville": [-66.67, 140.02],
2592
- "Antarctica/Macquarie": [-54.5, 158.95],
2593
- "Antarctica/Mawson": [-67.6, 62.88],
2594
- "Antarctica/McMurdo": [-77.83, 166.6],
2595
- "Antarctica/Palmer": [-64.8, -64.1],
2596
- "Antarctica/Rothera": [-67.57, -68.13],
2597
- "Antarctica/Syowa": [-69.01, 39.59],
2598
- "Antarctica/Troll": [-72.01, 2.53],
2599
- "Antarctica/Vostok": [-78.4, 106.9],
2600
- "Arctic/Longyearbyen": [78, 16],
2601
- "Asia/Aden": [12.75, 45.2],
2602
- "Asia/Almaty": [43.25, 76.95],
2603
- "Asia/Amman": [31.95, 35.93],
2604
- "Asia/Anadyr": [64.75, 177.48],
2605
- "Asia/Aqtau": [44.52, 50.27],
2606
- "Asia/Aqtobe": [50.28, 57.17],
2607
- "Asia/Ashgabat": [37.95, 58.38],
2608
- "Asia/Atyrau": [47.12, 51.93],
2609
- "Asia/Baghdad": [33.35, 44.42],
2610
- "Asia/Bahrain": [26.38, 50.58],
2611
- "Asia/Baku": [40.38, 49.85],
2612
- "Asia/Bangkok": [13.75, 100.52],
2613
- "Asia/Barnaul": [53.37, 83.75],
2614
- "Asia/Beirut": [33.88, 35.5],
2615
- "Asia/Bishkek": [42.9, 74.6],
2616
- "Asia/Brunei": [4.93, 114.92],
2617
- "Asia/Chita": [52.05, 113.47],
2618
- "Asia/Colombo": [6.93, 79.85],
2619
- "Asia/Damascus": [33.5, 36.3],
2620
- "Asia/Dhaka": [23.72, 90.42],
2621
- "Asia/Dili": [-8.55, 125.58],
2622
- "Asia/Dubai": [25.3, 55.3],
2623
- "Asia/Dushanbe": [38.58, 68.8],
2624
- "Asia/Famagusta": [35.12, 33.95],
2625
- "Asia/Gaza": [31.5, 34.47],
2626
- "Asia/Hebron": [31.53, 35.09],
2627
- "Asia/Ho_Chi_Minh": [10.75, 106.67],
2628
- "Asia/Hong_Kong": [22.28, 114.15],
2629
- "Asia/Hovd": [48.02, 91.65],
2630
- "Asia/Irkutsk": [52.27, 104.33],
2631
- "Asia/Jakarta": [-6.17, 106.8],
2632
- "Asia/Jayapura": [-2.53, 140.7],
2633
- "Asia/Jerusalem": [31.78, 35.22],
2634
- "Asia/Kabul": [34.52, 69.2],
2635
- "Asia/Kamchatka": [53.02, 158.65],
2636
- "Asia/Karachi": [24.87, 67.05],
2637
- "Asia/Kathmandu": [27.72, 85.32],
2638
- "Asia/Khandyga": [62.66, 135.55],
2639
- "Asia/Kolkata": [22.53, 88.37],
2640
- "Asia/Krasnoyarsk": [56.02, 92.83],
2641
- "Asia/Kuala_Lumpur": [3.17, 101.7],
2642
- "Asia/Kuching": [1.55, 110.33],
2643
- "Asia/Kuwait": [29.33, 47.98],
2644
- "Asia/Macau": [22.2, 113.54],
2645
- "Asia/Magadan": [59.57, 150.8],
2646
- "Asia/Makassar": [-5.12, 119.4],
2647
- "Asia/Manila": [14.59, 120.97],
2648
- "Asia/Muscat": [23.6, 58.58],
2649
- "Asia/Nicosia": [35.17, 33.37],
2650
- "Asia/Novokuznetsk": [53.75, 87.12],
2651
- "Asia/Novosibirsk": [55.03, 82.92],
2652
- "Asia/Omsk": [55, 73.4],
2653
- "Asia/Oral": [51.22, 51.35],
2654
- "Asia/Phnom_Penh": [11.55, 104.92],
2655
- "Asia/Pontianak": [-0.03, 109.33],
2656
- "Asia/Pyongyang": [39.02, 125.75],
2657
- "Asia/Qatar": [25.28, 51.53],
2658
- "Asia/Qostanay": [53.2, 63.62],
2659
- "Asia/Qyzylorda": [44.8, 65.47],
2660
- "Asia/Riyadh": [24.63, 46.72],
2661
- "Asia/Sakhalin": [46.97, 142.7],
2662
- "Asia/Samarkand": [39.67, 66.8],
2663
- "Asia/Seoul": [37.55, 126.97],
2664
- "Asia/Shanghai": [31.23, 121.47],
2665
- "Asia/Singapore": [1.28, 103.85],
2666
- "Asia/Srednekolymsk": [67.47, 153.72],
2667
- "Asia/Taipei": [25.05, 121.5],
2668
- "Asia/Tashkent": [41.33, 69.3],
2669
- "Asia/Tbilisi": [41.72, 44.82],
2670
- "Asia/Tehran": [35.67, 51.43],
2671
- "Asia/Thimphu": [27.47, 89.65],
2672
- "Asia/Tokyo": [35.65, 139.74],
2673
- "Asia/Tomsk": [56.5, 84.97],
2674
- "Asia/Ulaanbaatar": [47.92, 106.88],
2675
- "Asia/Urumqi": [43.8, 87.58],
2676
- "Asia/Ust-Nera": [64.56, 143.23],
2677
- "Asia/Vientiane": [17.97, 102.6],
2678
- "Asia/Vladivostok": [43.17, 131.93],
2679
- "Asia/Yakutsk": [62, 129.67],
2680
- "Asia/Yangon": [16.78, 96.17],
2681
- "Asia/Yekaterinburg": [56.85, 60.6],
2682
- "Asia/Yerevan": [40.18, 44.5],
2683
- "Atlantic/Azores": [37.73, -25.67],
2684
- "Atlantic/Bermuda": [32.28, -64.77],
2685
- "Atlantic/Canary": [28.1, -15.4],
2686
- "Atlantic/Cape_Verde": [14.92, -23.52],
2687
- "Atlantic/Faroe": [62.02, -6.77],
2688
- "Atlantic/Madeira": [32.63, -16.9],
2689
- "Atlantic/Reykjavik": [64.15, -21.85],
2690
- "Atlantic/South_Georgia": [-54.27, -36.53],
2691
- "Atlantic/St_Helena": [-15.92, -5.7],
2692
- "Atlantic/Stanley": [-51.7, -57.85],
2693
- "Australia/Adelaide": [-34.92, 138.58],
2694
- "Australia/Brisbane": [-27.47, 153.03],
2695
- "Australia/Broken_Hill": [-31.95, 141.45],
2696
- "Australia/Darwin": [-12.47, 130.83],
2697
- "Australia/Eucla": [-31.72, 128.87],
2698
- "Australia/Hobart": [-42.88, 147.32],
2699
- "Australia/Lindeman": [-20.27, 149],
2700
- "Australia/Lord_Howe": [-31.55, 159.08],
2701
- "Australia/Melbourne": [-37.82, 144.97],
2702
- "Australia/Perth": [-31.95, 115.85],
2703
- "Australia/Sydney": [-33.87, 151.22],
2704
- "Europe/Amsterdam": [52.37, 4.9],
2705
- "Europe/Andorra": [42.5, 1.52],
2706
- "Europe/Astrakhan": [46.35, 48.05],
2707
- "Europe/Athens": [37.97, 23.72],
2708
- "Europe/Belgrade": [44.83, 20.5],
2709
- "Europe/Berlin": [52.5, 13.37],
2710
- "Europe/Bratislava": [48.15, 17.12],
2711
- "Europe/Brussels": [50.83, 4.33],
2712
- "Europe/Bucharest": [44.43, 26.1],
2713
- "Europe/Budapest": [47.5, 19.08],
2714
- "Europe/Busingen": [47.7, 8.68],
2715
- "Europe/Chisinau": [47, 28.83],
2716
- "Europe/Copenhagen": [55.67, 12.58],
2717
- "Europe/Dublin": [53.33, -6.25],
2718
- "Europe/Gibraltar": [36.13, -5.35],
2719
- "Europe/Guernsey": [49.45, -2.54],
2720
- "Europe/Helsinki": [60.17, 24.97],
2721
- "Europe/Isle_of_Man": [54.15, -4.47],
2722
- "Europe/Istanbul": [41.02, 28.97],
2723
- "Europe/Jersey": [49.18, -2.11],
2724
- "Europe/Kaliningrad": [54.72, 20.5],
2725
- "Europe/Kirov": [58.6, 49.65],
2726
- "Europe/Kyiv": [50.43, 30.52],
2727
- "Europe/Lisbon": [38.72, -9.13],
2728
- "Europe/Ljubljana": [46.05, 14.52],
2729
- "Europe/London": [51.51, -0.13],
2730
- "Europe/Luxembourg": [49.6, 6.15],
2731
- "Europe/Madrid": [40.4, -3.68],
2732
- "Europe/Malta": [35.9, 14.52],
2733
- "Europe/Mariehamn": [60.1, 19.95],
2734
- "Europe/Minsk": [53.9, 27.57],
2735
- "Europe/Monaco": [43.7, 7.38],
2736
- "Europe/Moscow": [55.76, 37.62],
2737
- "Europe/Oslo": [59.92, 10.75],
2738
- "Europe/Paris": [48.87, 2.33],
2739
- "Europe/Podgorica": [42.43, 19.27],
2740
- "Europe/Prague": [50.08, 14.43],
2741
- "Europe/Riga": [56.95, 24.1],
2742
- "Europe/Rome": [41.9, 12.48],
2743
- "Europe/Samara": [53.2, 50.15],
2744
- "Europe/San_Marino": [43.92, 12.47],
2745
- "Europe/Sarajevo": [43.87, 18.42],
2746
- "Europe/Saratov": [51.57, 46.03],
2747
- "Europe/Simferopol": [44.95, 34.1],
2748
- "Europe/Skopje": [41.98, 21.43],
2749
- "Europe/Sofia": [42.68, 23.32],
2750
- "Europe/Stockholm": [59.33, 18.05],
2751
- "Europe/Tallinn": [59.42, 24.75],
2752
- "Europe/Tirane": [41.33, 19.83],
2753
- "Europe/Ulyanovsk": [54.33, 48.4],
2754
- "Europe/Vaduz": [47.15, 9.52],
2755
- "Europe/Vatican": [41.9, 12.45],
2756
- "Europe/Vienna": [48.22, 16.33],
2757
- "Europe/Vilnius": [54.68, 25.32],
2758
- "Europe/Volgograd": [48.73, 44.42],
2759
- "Europe/Warsaw": [52.25, 21],
2760
- "Europe/Zagreb": [45.8, 15.97],
2761
- "Europe/Zurich": [47.38, 8.53],
2762
- "Indian/Antananarivo": [-18.92, 47.52],
2763
- "Indian/Chagos": [-7.33, 72.42],
2764
- "Indian/Christmas": [-10.42, 105.72],
2765
- "Indian/Cocos": [-12.17, 96.92],
2766
- "Indian/Comoro": [-11.68, 43.27],
2767
- "Indian/Kerguelen": [-49.35, 70.22],
2768
- "Indian/Mahe": [-4.67, 55.47],
2769
- "Indian/Maldives": [4.17, 73.5],
2770
- "Indian/Mauritius": [-20.17, 57.5],
2771
- "Indian/Mayotte": [-12.78, 45.23],
2772
- "Indian/Reunion": [-20.87, 55.47],
2773
- "Pacific/Apia": [-13.83, -171.73],
2774
- "Pacific/Auckland": [-36.87, 174.77],
2775
- "Pacific/Bougainville": [-6.22, 155.57],
2776
- "Pacific/Chatham": [-43.95, -176.55],
2777
- "Pacific/Chuuk": [7.42, 151.78],
2778
- "Pacific/Easter": [-27.15, -109.43],
2779
- "Pacific/Efate": [-17.67, 168.42],
2780
- "Pacific/Fakaofo": [-9.37, -171.23],
2781
- "Pacific/Fiji": [-18.13, 178.42],
2782
- "Pacific/Funafuti": [-8.52, 179.22],
2783
- "Pacific/Galapagos": [-0.9, -89.6],
2784
- "Pacific/Gambier": [-23.13, -134.95],
2785
- "Pacific/Guadalcanal": [-9.53, 160.2],
2786
- "Pacific/Guam": [13.47, 144.75],
2787
- "Pacific/Honolulu": [21.31, -157.86],
2788
- "Pacific/Kanton": [-2.78, -171.72],
2789
- "Pacific/Kiritimati": [1.87, -157.33],
2790
- "Pacific/Kosrae": [5.32, 162.98],
2791
- "Pacific/Kwajalein": [9.08, 167.33],
2792
- "Pacific/Majuro": [7.15, 171.2],
2793
- "Pacific/Marquesas": [-9, -139.5],
2794
- "Pacific/Midway": [28.22, -177.37],
2795
- "Pacific/Nauru": [-0.52, 166.92],
2796
- "Pacific/Niue": [-19.02, -169.92],
2797
- "Pacific/Norfolk": [-29.05, 167.97],
2798
- "Pacific/Noumea": [-22.27, 166.45],
2799
- "Pacific/Pago_Pago": [-14.27, -170.7],
2800
- "Pacific/Palau": [7.33, 134.48],
2801
- "Pacific/Pitcairn": [-25.07, -130.08],
2802
- "Pacific/Pohnpei": [6.97, 158.22],
2803
- "Pacific/Port_Moresby": [-9.5, 147.17],
2804
- "Pacific/Rarotonga": [-21.23, -159.77],
2805
- "Pacific/Saipan": [15.2, 145.75],
2806
- "Pacific/Tahiti": [-17.53, -149.57],
2807
- "Pacific/Tarawa": [1.42, 173],
2808
- "Pacific/Tongatapu": [-21.13, -175.2],
2809
- "Pacific/Wake": [19.28, 166.62],
2810
- "Pacific/Wallis": [-13.3, -176.17]
2811
- };
2812
- var ALIASES = {
2813
- "Africa/Asmera": "Africa/Asmara",
2814
- "America/Buenos_Aires": "America/Argentina/Buenos_Aires",
2815
- "America/Catamarca": "America/Argentina/Catamarca",
2816
- "America/Coral_Harbour": "America/Atikokan",
2817
- "America/Cordoba": "America/Argentina/Cordoba",
2818
- "America/Godthab": "America/Nuuk",
2819
- "America/Indianapolis": "America/Indiana/Indianapolis",
2820
- "America/Jujuy": "America/Argentina/Jujuy",
2821
- "America/Louisville": "America/Kentucky/Louisville",
2822
- "America/Mendoza": "America/Argentina/Mendoza",
2823
- "Asia/Calcutta": "Asia/Kolkata",
2824
- "Asia/Katmandu": "Asia/Kathmandu",
2825
- "Asia/Rangoon": "Asia/Yangon",
2826
- "Asia/Saigon": "Asia/Ho_Chi_Minh",
2827
- "Atlantic/Faeroe": "Atlantic/Faroe",
2828
- "Europe/Kiev": "Europe/Kyiv",
2829
- "Pacific/Enderbury": "Pacific/Kanton",
2830
- "Pacific/Ponape": "Pacific/Pohnpei",
2831
- "Pacific/Truk": "Pacific/Chuuk"
2832
- };
2833
- function placeForZone(zone) {
2834
- const coords = COORDS[zone] ?? COORDS[ALIASES[zone]];
2835
- if (!coords) return null;
2836
- return { zone, lat: coords[0], lon: coords[1], label: zoneLabel(zone) };
2837
- }
2838
- var PLACE_OPTIONS = Object.keys(COORDS).map((zone) => ({ value: zone, label: zoneLabel(zone) })).sort((a, b) => a.label.localeCompare(b.label));
2839
-
2840
- // src/lib/store.ts
2841
- var import_react2 = __toESM(require_react(), 1);
2842
-
2843
- // src/lib/notify.ts
2844
- var import_react = __toESM(require_react(), 1);
2845
-
2846
- // src/lib/mutate.ts
2847
- function applyUpdate(events, id, patch) {
2848
- return events.map((e) => e.id === id ? { ...e, ...patch } : e);
2849
- }
2850
- function applyToggleDone(events, id, at) {
2851
- return events.map((e) => e.id === id ? { ...e, doneKeys: toggleDoneKey(e.doneKeys, at) } : e);
2852
- }
2853
- function applyRemove(events, id) {
2854
- return events.filter((e) => e.id !== id && e.seriesId !== id);
2855
- }
2856
- function applySplit(events, id, at, tail, mintId) {
2857
- const original = events.find((e) => e.id === id);
2858
- if (!original) return { events, created: null };
2859
- const split = splitRule(original, original.start, at);
2860
- const created = { ...tail, ...split.tail, seriesId: "", seriesKey: "", id: mintId() };
2861
- const seed = new Date(original.start);
2862
- const detached = (e) => e.seriesId === id && (keyToInstant(e.seriesKey, seed) ?? -Infinity) >= at;
2863
- return {
2864
- events: [
2865
- ...events.map(
2866
- (e) => e.id === id ? { ...e, ...split.head } : detached(e) ? { ...e, seriesId: created.id } : e
2867
- ),
2868
- created
2869
- ],
2870
- created
2871
- };
2872
- }
2873
- function applyTruncate(events, id, at) {
2874
- const original = events.find((e) => e.id === id);
2875
- if (!original) return events;
2876
- const split = splitRule(original, original.start, at);
2877
- const seed = new Date(original.start);
2878
- 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
+ };
2879
2482
  }
2880
2483
 
2881
2484
  // src/lib/quickadd.ts
@@ -2906,6 +2509,16 @@ function parseDate(raw, reference) {
2906
2509
  const delta = (weekdayIndex - base.getDay() + 7) % 7 || 7;
2907
2510
  return addDays(base, delta);
2908
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
+ }
2909
2522
  const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(query);
2910
2523
  if (iso) {
2911
2524
  const year = Number(iso[1]);
@@ -2919,11 +2532,23 @@ function parseDate(raw, reference) {
2919
2532
  const resolveMonthDay = (monthStr, domStr) => {
2920
2533
  const monthIndex = MONTHS_LOWER.findIndex((m) => m === monthStr || m.slice(0, 3) === monthStr);
2921
2534
  const dom = Number(domStr);
2922
- if (monthIndex === -1 || dom < 1 || dom > daysInMonth(reference.getFullYear(), monthIndex))
2923
- return null;
2535
+ if (monthIndex === -1 || dom < 1) return null;
2924
2536
  const year = reference.getFullYear();
2925
- const candidate = new Date(year, monthIndex, dom);
2926
- 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;
2927
2552
  };
2928
2553
  const monthDay = /^([a-z]+)\s+(\d{1,2})$/.exec(query);
2929
2554
  if (monthDay) {
@@ -3165,8 +2790,467 @@ function parseQuickAdd(input, reference, now = /* @__PURE__ */ new Date(), fallb
3165
2790
  };
3166
2791
  }
3167
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
+
3168
3249
  // src/lib/store.ts
3169
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,
3170
3254
  // The 0–23 face. It is the one that matches the instrument: a revolution is
3171
3255
  // a day here, not half of one, so an hour has exactly one place on it and
3172
3256
  // `13` is where `13` is. AM/PM is a preference, and it is one click away.
@@ -3286,9 +3370,33 @@ var DEFAULT_SETTINGS = {
3286
3370
  wakingTo: DEFAULT_WAKING.to
3287
3371
  };
3288
3372
  var DEFAULT_CALENDARS = [
3289
- { id: "personal", name: "Personal", color: PALETTE[0], icon: "house", visible: true },
3290
- { id: "work", name: "Work", color: PALETTE[3], icon: "briefcase", visible: true },
3291
- { 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
+ }
3292
3400
  ];
3293
3401
  function newId() {
3294
3402
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -3331,10 +3439,26 @@ function draftEvent(day, startMin, endMin, calendarId, repeat = "never") {
3331
3439
  // somebody else wrote.
3332
3440
  organizer: null,
3333
3441
  attendees: [],
3334
- 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
3335
3448
  };
3336
3449
  }
3337
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
+
3338
3462
  // src/lib/backup.ts
3339
3463
  function readSettings(value) {
3340
3464
  if (!isRecord(value)) return DEFAULT_SETTINGS;
@@ -3344,6 +3468,10 @@ function readSettings(value) {
3344
3468
  // there are exactly two spellings and both are written out by every
3345
3469
  // version that has ever had the setting.
3346
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,
3347
3475
  secondZone: typeof value.secondZone === "string" ? value.secondZone : "",
3348
3476
  // The zone the browser that wrote this backup was in. Carried rather than
3349
3477
  // dropped, and deliberately not replaced with the reader's own: a Berlin
@@ -3506,7 +3634,12 @@ function readEvent(value) {
3506
3634
  // did — clamped up to the list it labels, since a blob understating it
3507
3635
  // would print a negative remainder.
3508
3636
  organizer: readPerson(value.organizer),
3509
- ...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)
3510
3643
  };
3511
3644
  }
3512
3645
  function readCalendar(value) {
@@ -3524,7 +3657,14 @@ function readCalendar(value) {
3524
3657
  // again should not lose the mark — but an unbounded string is a paste, and
3525
3658
  // an over-long *name* is now dropped rather than cut into junk.
3526
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),
3527
3666
  visible: value.visible !== false,
3667
+ updatedAt: readStamp(value.updatedAt),
3528
3668
  ...feedUrl ? { feed: { url: feedUrl, fetchedAt: 0, error: "" } } : {}
3529
3669
  };
3530
3670
  }
@@ -3543,12 +3683,17 @@ function readBackup(parsed) {
3543
3683
  return e.seriesId && !seriesIds.has(e.seriesId) ? { ...parented, seriesId: "", seriesKey: "" } : parented;
3544
3684
  }),
3545
3685
  calendars,
3546
- 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)
3547
3692
  };
3548
3693
  }
3549
3694
 
3550
3695
  // src/lib/mirror.ts
3551
- var MIRROR_VERSION = 1;
3696
+ var MIRROR_VERSION = 2;
3552
3697
  function settled(calendars) {
3553
3698
  return calendars.map(
3554
3699
  (c) => c.feed ? { ...c, feed: { ...c.feed, fetchedAt: 0, error: "" } } : c
@@ -3563,7 +3708,9 @@ function serialiseMirror(s, rev, zone = localZone()) {
3563
3708
  rev,
3564
3709
  events: s.events,
3565
3710
  calendars: settled(s.calendars),
3566
- settings: s.settings
3711
+ settings: s.settings,
3712
+ settingsAt: s.settingsAt,
3713
+ graves: s.graves
3567
3714
  };
3568
3715
  return `${JSON.stringify(file, null, 2)}
3569
3716
  `;
@@ -3582,6 +3729,11 @@ function readMirrorFile(text) {
3582
3729
  events: read.events,
3583
3730
  calendars: read.calendars,
3584
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,
3585
3737
  rev: typeof envelope.rev === "number" && Number.isFinite(envelope.rev) ? envelope.rev : 0,
3586
3738
  // A plain `.json` backup has no zone. Absent means "written by something
3587
3739
  // that did not say", which the reader treats as agreeing with it — the
@@ -3594,30 +3746,128 @@ function readMirrorFile(text) {
3594
3746
  };
3595
3747
  }
3596
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
+
3597
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";
3598
3848
  var RefusalError = class extends Error {
3599
3849
  };
3600
3850
  function resolveFilePath(argv2, env) {
3601
3851
  const flagIndex = argv2.indexOf("--file");
3602
- const flagValue = flagIndex >= 0 ? argv2[flagIndex + 1] : void 0;
3603
- const path2 = flagValue || env.CIRCAL_FILE;
3604
- if (!path2) {
3852
+ const flagValue2 = flagIndex >= 0 ? argv2[flagIndex + 1] : void 0;
3853
+ const path = flagValue2 || env.CIRCAL_FILE;
3854
+ if (!path) {
3605
3855
  throw new Error(
3606
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."
3607
3857
  );
3608
3858
  }
3609
- return path2;
3859
+ return path;
3610
3860
  }
3611
- async function readMirror(path2) {
3612
- const text = await readFile(path2, "utf8");
3861
+ async function readMirror(path) {
3862
+ const text = await readFile(path, "utf8");
3613
3863
  const contents = readMirrorFile(text);
3614
3864
  if (!contents) {
3615
- 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).`);
3616
3866
  }
3617
3867
  return contents;
3618
3868
  }
3619
- async function atomicWrite(path2, body) {
3620
- const dir = dirname(path2);
3869
+ async function atomicWrite(path, body) {
3870
+ const dir = dirname(path);
3621
3871
  const tmp = join(dir, `.circal-mcp-${randomBytes(6).toString("hex")}.tmp`);
3622
3872
  try {
3623
3873
  const file = await open(tmp, "w");
@@ -3627,7 +3877,7 @@ async function atomicWrite(path2, body) {
3627
3877
  } finally {
3628
3878
  await file.close();
3629
3879
  }
3630
- await rename(tmp, path2);
3880
+ await rename(tmp, path);
3631
3881
  } catch (error) {
3632
3882
  await rm(tmp, { force: true });
3633
3883
  throw error;
@@ -3661,15 +3911,21 @@ function assertVersionAllowsMutation(contents) {
3661
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.`
3662
3912
  );
3663
3913
  }
3664
- async function loadForRead(ctx2) {
3665
- const contents = await readMirror(ctx2.path);
3914
+ async function loadForRead(ctx) {
3915
+ const contents = await readMirror(ctx.path);
3666
3916
  return { contents, warning: zoneWarning(contents) };
3667
3917
  }
3918
+ function fileStore(ctx) {
3919
+ return {
3920
+ load: () => loadForRead(ctx),
3921
+ write: (mutate) => casWrite(ctx, mutate)
3922
+ };
3923
+ }
3668
3924
  var LOCK_STALE_MS = 1e4;
3669
3925
  var LOCK_WAIT_MS = 2e3;
3670
3926
  var LOCK_POLL_MS = 100;
3671
- async function acquireLock(path2) {
3672
- const lock = `${path2}.lock`;
3927
+ async function acquireLock(path) {
3928
+ const lock = `${path}.lock`;
3673
3929
  const deadline = Date.now() + LOCK_WAIT_MS;
3674
3930
  for (; ; ) {
3675
3931
  try {
@@ -3700,15 +3956,15 @@ async function acquireLock(path2) {
3700
3956
  await sleep(LOCK_POLL_MS);
3701
3957
  }
3702
3958
  }
3703
- async function casWriteOnce(ctx2, mutate) {
3704
- const lock = await acquireLock(ctx2.path);
3959
+ async function casWriteOnce(ctx, mutate) {
3960
+ const lock = await acquireLock(ctx.path);
3705
3961
  try {
3706
3962
  for (let attempt = 0; attempt < 2; attempt++) {
3707
- const before = await readMirror(ctx2.path);
3963
+ const before = await readMirror(ctx.path);
3708
3964
  assertVersionAllowsMutation(before);
3709
- assertZoneAllowsMutation(before, ctx2.env);
3965
+ assertZoneAllowsMutation(before, ctx.env);
3710
3966
  const { result, summary } = mutate(before);
3711
- const after = await readMirror(ctx2.path);
3967
+ const after = await readMirror(ctx.path);
3712
3968
  if (after.rev !== before.rev) {
3713
3969
  if (attempt === 1) {
3714
3970
  throw new Error(
@@ -3718,7 +3974,7 @@ async function casWriteOnce(ctx2, mutate) {
3718
3974
  continue;
3719
3975
  }
3720
3976
  await atomicWrite(
3721
- ctx2.path,
3977
+ ctx.path,
3722
3978
  serialiseMirror(result, after.rev + 1, after.zone || localZone())
3723
3979
  );
3724
3980
  return summary;
@@ -3729,8 +3985,8 @@ async function casWriteOnce(ctx2, mutate) {
3729
3985
  }
3730
3986
  }
3731
3987
  var writeQueue = Promise.resolve();
3732
- function casWrite(ctx2, mutate) {
3733
- const run = writeQueue.then(() => casWriteOnce(ctx2, mutate));
3988
+ function casWrite(ctx, mutate) {
3989
+ const run = writeQueue.then(() => casWriteOnce(ctx, mutate));
3734
3990
  writeQueue = run.then(
3735
3991
  () => void 0,
3736
3992
  () => void 0
@@ -3738,33 +3994,409 @@ function casWrite(ctx2, mutate) {
3738
3994
  return run;
3739
3995
  }
3740
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
+
3741
4373
  // mcp/find.ts
3742
- import { readFile as readFile2, readdir, stat as stat2 } from "node:fs/promises";
3743
- import { homedir } from "node:os";
3744
- 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";
3745
4377
  var SKIP_DIR_NAMES = { node_modules: true, Library: true, ".git": true };
3746
4378
  var MAX_FILE_BYTES = 32 * 1024 * 1024;
3747
4379
  function roots(home) {
3748
4380
  return [
3749
4381
  home,
3750
- join2(home, "Documents"),
3751
- join2(home, "Desktop"),
3752
- join2(home, "Downloads"),
3753
- 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"),
3754
4386
  // iCloud Drive (Documents)
3755
- join2(home, "Dropbox"),
3756
- join2(home, "Sync")
4387
+ join3(home, "Dropbox"),
4388
+ join3(home, "Sync")
3757
4389
  ];
3758
4390
  }
3759
4391
  function appDefault(home) {
3760
- return join2(home, "Library", "Application Support", "circal", "calendar.json");
4392
+ return join3(home, "Library", "Application Support", "circal", "calendar.json");
3761
4393
  }
3762
4394
  async function jsonFilesUnder(root) {
3763
4395
  const found = [];
3764
4396
  const top = await listDir(root);
3765
4397
  for (const entry of top) {
3766
4398
  if (entry.name.startsWith(".")) continue;
3767
- const full = join2(root, entry.name);
4399
+ const full = join3(root, entry.name);
3768
4400
  if (entry.isFile()) {
3769
4401
  if (entry.name.toLowerCase().endsWith(".json")) found.push(full);
3770
4402
  continue;
@@ -3772,39 +4404,39 @@ async function jsonFilesUnder(root) {
3772
4404
  if (!entry.isDirectory() || SKIP_DIR_NAMES[entry.name]) continue;
3773
4405
  for (const sub of await listDir(full)) {
3774
4406
  if (sub.name.startsWith(".") || !sub.isFile()) continue;
3775
- 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));
3776
4408
  }
3777
4409
  }
3778
4410
  return found;
3779
4411
  }
3780
- async function listDir(path2) {
4412
+ async function listDir(path) {
3781
4413
  try {
3782
- return await readdir(path2, { withFileTypes: true });
4414
+ return await readdir(path, { withFileTypes: true });
3783
4415
  } catch {
3784
4416
  return [];
3785
4417
  }
3786
4418
  }
3787
- async function asMirror(path2) {
4419
+ async function asMirror(path) {
3788
4420
  try {
3789
- const info = await stat2(path2);
4421
+ const info = await stat2(path);
3790
4422
  if (!info.isFile() || info.size > MAX_FILE_BYTES) return null;
3791
- const text = await readFile2(path2, "utf8");
4423
+ const text = await readFile3(path, "utf8");
3792
4424
  const contents = readMirrorFile(text);
3793
- return contents ? { path: path2, contents } : null;
4425
+ return contents ? { path, contents } : null;
3794
4426
  } catch {
3795
4427
  return null;
3796
4428
  }
3797
4429
  }
3798
- async function runFind(home = homedir()) {
4430
+ async function runFind(home = homedir2()) {
3799
4431
  const searchRoots = roots(home);
3800
4432
  const candidates = /* @__PURE__ */ new Set();
3801
4433
  candidates.add(appDefault(home));
3802
4434
  for (const root of searchRoots) {
3803
- for (const path2 of await jsonFilesUnder(root)) candidates.add(path2);
4435
+ for (const path of await jsonFilesUnder(root)) candidates.add(path);
3804
4436
  }
3805
4437
  const hits = [];
3806
- for (const path2 of [...candidates].sort()) {
3807
- const hit = await asMirror(path2);
4438
+ for (const path of [...candidates].sort()) {
4439
+ const hit = await asMirror(path);
3808
4440
  if (hit) hits.push(hit);
3809
4441
  }
3810
4442
  if (hits.length === 0) {
@@ -4069,7 +4701,11 @@ function buildEventRecord(input, calendars, settings) {
4069
4701
  // assembled would be a list of people who were never asked.
4070
4702
  organizer: null,
4071
4703
  attendees: [],
4072
- 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
4073
4709
  });
4074
4710
  }
4075
4711
  function applyPatchToEvent(base, input, calendars, settings, anchorMs = base.start) {
@@ -4109,9 +4745,21 @@ function applyPatchToEvent(base, input, calendars, settings, anchorMs = base.sta
4109
4745
  // had asked and who was coming.
4110
4746
  organizer: base.organizer,
4111
4747
  attendees: base.attendees,
4112
- 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
4113
4752
  });
4114
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
+ }
4115
4763
  function calendarName(calendars, id) {
4116
4764
  return calendars.find((c) => c.id === id)?.name ?? "\u2014";
4117
4765
  }
@@ -4245,25 +4893,17 @@ function performQuickAdd(contents, text, date) {
4245
4893
  title: parsed.title,
4246
4894
  ...parsed.repeat
4247
4895
  });
4248
- const event = { ...record, id: newId() };
4896
+ const event = { ...record, id: newId(), updatedAt: Date.now() };
4249
4897
  return {
4250
- mutated: {
4251
- events: [...contents.events, event],
4252
- calendars: contents.calendars,
4253
- settings: contents.settings
4254
- },
4898
+ mutated: { ...carried(contents), events: [...contents.events, event] },
4255
4899
  message: `Created ${eventSummaryLine(event, contents.calendars, contents.settings.clock)}`
4256
4900
  };
4257
4901
  }
4258
4902
  function performCreateEvent(contents, input) {
4259
4903
  const record = buildEventRecord(input, contents.calendars, contents.settings);
4260
- const event = { ...record, id: newId() };
4904
+ const event = { ...record, id: newId(), updatedAt: Date.now() };
4261
4905
  return {
4262
- mutated: {
4263
- events: [...contents.events, event],
4264
- calendars: contents.calendars,
4265
- settings: contents.settings
4266
- },
4906
+ mutated: { ...carried(contents), events: [...contents.events, event] },
4267
4907
  message: `Created ${eventSummaryLine(event, contents.calendars, contents.settings.clock)}`
4268
4908
  };
4269
4909
  }
@@ -4275,10 +4915,10 @@ function performUpdateEvent(contents, id, scopeArg, occurrence, patch) {
4275
4915
  const clock = contents.settings.clock;
4276
4916
  if (scope === "all") {
4277
4917
  const record2 = applyPatchToEvent(original, patch, contents.calendars, contents.settings);
4278
- const events = applyUpdate(contents.events, original.id, record2);
4918
+ const events = applyUpdate(contents.events, original.id, record2, Date.now());
4279
4919
  const updated = events.find((e) => e.id === original.id);
4280
4920
  return {
4281
- mutated: { events, calendars: contents.calendars, settings: contents.settings },
4921
+ mutated: { ...carried(contents), events },
4282
4922
  message: `Updated ${eventSummaryLine(updated, contents.calendars, clock)}`
4283
4923
  };
4284
4924
  }
@@ -4290,22 +4930,22 @@ function performUpdateEvent(contents, id, scopeArg, occurrence, patch) {
4290
4930
  ...NEVER,
4291
4931
  seriesId: original.id,
4292
4932
  seriesKey: instantKey(at),
4293
- 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()
4294
4938
  };
4295
4939
  return {
4296
- mutated: {
4297
- events: [...contents.events, override],
4298
- calendars: contents.calendars,
4299
- settings: contents.settings
4300
- },
4940
+ mutated: { ...carried(contents), events: [...contents.events, override] },
4301
4941
  message: `Updated one occurrence: ${eventSummaryLine(override, contents.calendars, clock)}`
4302
4942
  };
4303
4943
  }
4304
4944
  const record = applyPatchToEvent(original, patch, contents.calendars, contents.settings, at);
4305
- const split = applySplit(contents.events, original.id, at, record, newId);
4945
+ const split = applySplit(contents.events, original.id, at, record, newId, Date.now());
4306
4946
  if (!split.created) throw new RefusalError(`no event with id "${id}"`);
4307
4947
  return {
4308
- mutated: { events: split.events, calendars: contents.calendars, settings: contents.settings },
4948
+ mutated: { ...carried(contents), events: split.events },
4309
4949
  message: `Updated from ${dayKey(new Date(at))} on: ${eventSummaryLine(split.created, contents.calendars, clock)}`
4310
4950
  };
4311
4951
  }
@@ -4316,30 +4956,38 @@ function performDeleteEvent(contents, id, scopeArg, occurrence) {
4316
4956
  const scope = requireScope(original, scopeArg);
4317
4957
  const title = original.title || "Untitled";
4318
4958
  if (scope === "all") {
4959
+ const removed = applyRemove(contents.events, original.id, Date.now());
4319
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.
4320
4964
  mutated: {
4321
- events: applyRemove(contents.events, original.id),
4322
- calendars: contents.calendars,
4323
- settings: contents.settings
4965
+ ...carried(contents),
4966
+ events: removed.events,
4967
+ graves: [...contents.graves, ...removed.graves]
4324
4968
  },
4325
4969
  message: `Deleted "${title}" (the whole series).`
4326
4970
  };
4327
4971
  }
4328
4972
  const at = resolveOccurrenceRequired(original, occurrence, scope);
4329
4973
  if (scope === "one") {
4330
- const events = applyUpdate(contents.events, original.id, {
4331
- exdates: [...original.exdates, instantKey(at)]
4332
- });
4974
+ const events = applyUpdate(
4975
+ contents.events,
4976
+ original.id,
4977
+ { exdates: [...original.exdates, instantKey(at)] },
4978
+ Date.now()
4979
+ );
4333
4980
  return {
4334
- mutated: { events, calendars: contents.calendars, settings: contents.settings },
4981
+ mutated: { ...carried(contents), events },
4335
4982
  message: `Deleted "${title}" on ${dayKey(new Date(at))} only \u2014 the series continues.`
4336
4983
  };
4337
4984
  }
4985
+ const cut = applyTruncate(contents.events, original.id, at, Date.now());
4338
4986
  return {
4339
4987
  mutated: {
4340
- events: applyTruncate(contents.events, original.id, at),
4341
- calendars: contents.calendars,
4342
- settings: contents.settings
4988
+ ...carried(contents),
4989
+ events: cut.events,
4990
+ graves: [...contents.graves, ...cut.graves]
4343
4991
  },
4344
4992
  message: `Deleted "${title}" from ${dayKey(new Date(at))} on.`
4345
4993
  };
@@ -4351,9 +4999,9 @@ function performToggleDone(contents, id, occurrence) {
4351
4999
  refuseIfFeed(original, contents.calendars);
4352
5000
  const at = resolveInstant(original, occurrence);
4353
5001
  const wasDone = original.doneKeys.includes(instantKey(at));
4354
- const events = applyToggleDone(contents.events, original.id, at);
5002
+ const events = applyToggleDone(contents.events, original.id, at, Date.now());
4355
5003
  return {
4356
- mutated: { events, calendars: contents.calendars, settings: contents.settings },
5004
+ mutated: { ...carried(contents), events },
4357
5005
  message: `${wasDone ? "Reopened" : "Completed"} "${original.title || "Untitled"}" on ${dayKey(new Date(at))}.`
4358
5006
  };
4359
5007
  }
@@ -4365,15 +5013,12 @@ function performCreateCalendar(contents, name, color, icon) {
4365
5013
  name: trimmed,
4366
5014
  color: color || PALETTE[contents.calendars.length * 3 % PALETTE.length],
4367
5015
  icon: icon ?? "",
4368
- visible: true
5016
+ visible: true,
5017
+ updatedAt: Date.now()
4369
5018
  });
4370
5019
  if (!calendar) throw new RefusalError("the assembled calendar failed validation");
4371
5020
  return {
4372
- mutated: {
4373
- events: contents.events,
4374
- calendars: [...contents.calendars, calendar],
4375
- settings: contents.settings
4376
- },
5021
+ mutated: { ...carried(contents), calendars: [...contents.calendars, calendar] },
4377
5022
  message: `Created calendar "${calendar.name}" \u2014 id=${calendar.id}`
4378
5023
  };
4379
5024
  }
@@ -4461,7 +5106,7 @@ var SCOPE_DESCRIPTION = 'Required when the target event repeats: "one" (just thi
4461
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".';
4462
5107
  var scopeSchema = z.enum(["one", "future", "all"]).describe(SCOPE_DESCRIPTION);
4463
5108
  var occurrenceSchema = z.string().describe(OCCURRENCE_DESCRIPTION);
4464
- function registerTools(server, ctx2) {
5109
+ function registerTools(server, store3) {
4465
5110
  server.registerTool(
4466
5111
  "list_day",
4467
5112
  {
@@ -4470,7 +5115,7 @@ function registerTools(server, ctx2) {
4470
5115
  annotations: { readOnlyHint: true }
4471
5116
  },
4472
5117
  async ({ date }) => guarded(async () => {
4473
- const { contents, warning } = await loadForRead(ctx2);
5118
+ const { contents, warning } = await store3.load();
4474
5119
  return withWarning(listDay(contents, date), warning);
4475
5120
  })
4476
5121
  );
@@ -4485,7 +5130,7 @@ function registerTools(server, ctx2) {
4485
5130
  annotations: { readOnlyHint: true }
4486
5131
  },
4487
5132
  async ({ from, to }) => guarded(async () => {
4488
- const { contents, warning } = await loadForRead(ctx2);
5133
+ const { contents, warning } = await store3.load();
4489
5134
  return withWarning(listRange(contents, from, to), warning);
4490
5135
  })
4491
5136
  );
@@ -4500,7 +5145,7 @@ function registerTools(server, ctx2) {
4500
5145
  annotations: { readOnlyHint: true }
4501
5146
  },
4502
5147
  async ({ query, limit }) => guarded(async () => {
4503
- const { contents, warning } = await loadForRead(ctx2);
5148
+ const { contents, warning } = await store3.load();
4504
5149
  return withWarning(searchEvents(contents, query, limit), warning);
4505
5150
  })
4506
5151
  );
@@ -4517,7 +5162,7 @@ function registerTools(server, ctx2) {
4517
5162
  annotations: { readOnlyHint: true }
4518
5163
  },
4519
5164
  async ({ date, minutes, from, to }) => guarded(async () => {
4520
- const { contents, warning } = await loadForRead(ctx2);
5165
+ const { contents, warning } = await store3.load();
4521
5166
  return withWarning(freeSlots(contents, date, minutes, from, to), warning);
4522
5167
  })
4523
5168
  );
@@ -4529,7 +5174,7 @@ function registerTools(server, ctx2) {
4529
5174
  annotations: { readOnlyHint: true }
4530
5175
  },
4531
5176
  async () => guarded(async () => {
4532
- const { contents, warning } = await loadForRead(ctx2);
5177
+ const { contents, warning } = await store3.load();
4533
5178
  return withWarning(listCalendars(contents), warning);
4534
5179
  })
4535
5180
  );
@@ -4541,7 +5186,7 @@ function registerTools(server, ctx2) {
4541
5186
  annotations: { readOnlyHint: true }
4542
5187
  },
4543
5188
  async ({ id }) => guarded(async () => {
4544
- const { contents, warning } = await loadForRead(ctx2);
5189
+ const { contents, warning } = await store3.load();
4545
5190
  return withWarning(describeEvent(contents, id), warning);
4546
5191
  })
4547
5192
  );
@@ -4558,7 +5203,7 @@ function registerTools(server, ctx2) {
4558
5203
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
4559
5204
  },
4560
5205
  async ({ text, date }) => guarded(
4561
- () => casWrite(ctx2, (contents) => {
5206
+ () => store3.write((contents) => {
4562
5207
  const outcome = performQuickAdd(contents, text, date);
4563
5208
  return { result: outcome.mutated, summary: outcome.message };
4564
5209
  })
@@ -4572,7 +5217,7 @@ function registerTools(server, ctx2) {
4572
5217
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
4573
5218
  },
4574
5219
  async (input) => guarded(
4575
- () => casWrite(ctx2, (contents) => {
5220
+ () => store3.write((contents) => {
4576
5221
  const outcome = performCreateEvent(contents, input);
4577
5222
  return { result: outcome.mutated, summary: outcome.message };
4578
5223
  })
@@ -4591,7 +5236,7 @@ function registerTools(server, ctx2) {
4591
5236
  annotations: { destructiveHint: true, idempotentHint: false }
4592
5237
  },
4593
5238
  async ({ id, scope, occurrence, patch }) => guarded(
4594
- () => casWrite(ctx2, (contents) => {
5239
+ () => store3.write((contents) => {
4595
5240
  const outcome = performUpdateEvent(contents, id, scope, occurrence, patch);
4596
5241
  return { result: outcome.mutated, summary: outcome.message };
4597
5242
  })
@@ -4609,7 +5254,7 @@ function registerTools(server, ctx2) {
4609
5254
  annotations: { destructiveHint: true, idempotentHint: false }
4610
5255
  },
4611
5256
  async ({ id, scope, occurrence }) => guarded(
4612
- () => casWrite(ctx2, (contents) => {
5257
+ () => store3.write((contents) => {
4613
5258
  const outcome = performDeleteEvent(contents, id, scope, occurrence);
4614
5259
  return { result: outcome.mutated, summary: outcome.message };
4615
5260
  })
@@ -4626,7 +5271,7 @@ function registerTools(server, ctx2) {
4626
5271
  annotations: { destructiveHint: false, idempotentHint: false }
4627
5272
  },
4628
5273
  async ({ id, occurrence }) => guarded(
4629
- () => casWrite(ctx2, (contents) => {
5274
+ () => store3.write((contents) => {
4630
5275
  const outcome = performToggleDone(contents, id, occurrence);
4631
5276
  return { result: outcome.mutated, summary: outcome.message };
4632
5277
  })
@@ -4644,7 +5289,7 @@ function registerTools(server, ctx2) {
4644
5289
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
4645
5290
  },
4646
5291
  async ({ name, color, icon }) => guarded(
4647
- () => casWrite(ctx2, (contents) => {
5292
+ () => store3.write((contents) => {
4648
5293
  const outcome = performCreateCalendar(contents, name, color, icon);
4649
5294
  return { result: outcome.mutated, summary: outcome.message };
4650
5295
  })
@@ -4653,10 +5298,18 @@ function registerTools(server, ctx2) {
4653
5298
  }
4654
5299
 
4655
5300
  // mcp/index.ts
4656
- 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)
4657
5302
 
4658
5303
  Usage:
4659
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
4660
5313
  circal-mcp --find Search common locations for a mirror file and
4661
5314
  print a ready-to-paste client config (read-only,
4662
5315
  does not start the server)
@@ -4664,8 +5317,10 @@ Usage:
4664
5317
 
4665
5318
  Environment:
4666
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)
4667
5322
  CIRCAL_ALLOW_ZONE_MISMATCH Set to 1 to allow writes when this process's
4668
- zone does not match the file's recorded zone
5323
+ zone does not match the document's recorded zone
4669
5324
  `;
4670
5325
  var argv = process.argv.slice(2);
4671
5326
  if (argv.includes("--help") || argv.includes("-h")) {
@@ -4676,18 +5331,41 @@ if (argv.includes("--find")) {
4676
5331
  await runFind();
4677
5332
  process.exit(0);
4678
5333
  }
4679
- var path;
4680
- try {
4681
- path = resolveFilePath(argv, process.env);
4682
- } catch (error) {
4683
- const message = error instanceof Error ? error.message : String(error);
4684
- console.error(`${message} Run \`circal-mcp --find\` to search common locations for one.`);
4685
- 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 });
4686
5365
  }
4687
- var ctx = { path, env: process.env };
4688
5366
  function createServer() {
4689
5367
  const server = new McpServer({ name: "circal", version: "1.0.0" });
4690
- registerTools(server, ctx);
5368
+ registerTools(server, store2);
4691
5369
  return server;
4692
5370
  }
4693
5371
  void serveStdio(createServer);