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