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