circal-mcp 0.2.1 → 0.4.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 +86 -28
- package/dist/circal-mcp.mjs +2122 -823
- 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) {
|
|
@@ -2288,10 +2332,9 @@ function occurrencesOn(events, calendars, day) {
|
|
|
2288
2332
|
continuesAfter,
|
|
2289
2333
|
dayIndex: daysBetween(firstDay, dayStart) + 1,
|
|
2290
2334
|
dayCount: daysBetween(firstDay, lastDay) + 1,
|
|
2291
|
-
//
|
|
2292
|
-
//
|
|
2293
|
-
|
|
2294
|
-
done: event.doneKeys.length > 0 && event.doneKeys.includes(instantKey(start)),
|
|
2335
|
+
// An event is never done — the field exists for a task's occurrences,
|
|
2336
|
+
// resolved in `taskOccurrences`.
|
|
2337
|
+
done: false,
|
|
2295
2338
|
lane: 0
|
|
2296
2339
|
});
|
|
2297
2340
|
}
|
|
@@ -2303,22 +2346,99 @@ function packLanes(occurrences, pad2 = 6) {
|
|
|
2303
2346
|
const laneEnds = [];
|
|
2304
2347
|
return occurrences.map((occ) => {
|
|
2305
2348
|
if (occ.event.allDay) return occ;
|
|
2306
|
-
if (occ.
|
|
2349
|
+
if (occ.task && occ.startMin === occ.endMin) return occ;
|
|
2307
2350
|
let lane = laneEnds.findIndex((end) => end <= occ.startMin + pad2);
|
|
2308
2351
|
if (lane === -1) lane = laneEnds.length;
|
|
2309
2352
|
laneEnds[lane] = occ.endMin;
|
|
2310
2353
|
return { ...occ, lane };
|
|
2311
2354
|
});
|
|
2312
2355
|
}
|
|
2313
|
-
function dayAgenda(events, calendars, day) {
|
|
2314
|
-
const all = occurrencesOn(events, calendars, day);
|
|
2356
|
+
function dayAgenda(events, calendars, day, tasks = []) {
|
|
2357
|
+
const all = [...occurrencesOn(events, calendars, day), ...taskOccurrences(tasks, calendars, day)];
|
|
2358
|
+
all.sort((a, b) => a.start.getTime() - b.start.getTime() || a.end.getTime() - b.end.getTime());
|
|
2315
2359
|
const allDay = all.filter((o) => o.event.allDay);
|
|
2316
2360
|
const timed = packLanes(all.filter((o) => !o.event.allDay));
|
|
2317
2361
|
return { timed, allDay, all: [...allDay, ...timed] };
|
|
2318
2362
|
}
|
|
2319
|
-
function
|
|
2320
|
-
|
|
2321
|
-
|
|
2363
|
+
function taskEvent(task, start, end) {
|
|
2364
|
+
return {
|
|
2365
|
+
id: task.id,
|
|
2366
|
+
title: task.title,
|
|
2367
|
+
calendarId: task.calendarId,
|
|
2368
|
+
start,
|
|
2369
|
+
end,
|
|
2370
|
+
allDay: false,
|
|
2371
|
+
tzid: "",
|
|
2372
|
+
location: "",
|
|
2373
|
+
url: "",
|
|
2374
|
+
notes: task.notes,
|
|
2375
|
+
...NEVER,
|
|
2376
|
+
seriesId: "",
|
|
2377
|
+
seriesKey: "",
|
|
2378
|
+
color: task.color,
|
|
2379
|
+
icon: task.icon,
|
|
2380
|
+
free: false,
|
|
2381
|
+
remindLead: REMIND_INHERIT,
|
|
2382
|
+
organizer: null,
|
|
2383
|
+
attendees: [],
|
|
2384
|
+
attendeeCount: 0,
|
|
2385
|
+
updatedAt: 0
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
function taskOccurrences(tasks, calendars, day) {
|
|
2389
|
+
const hidden = new Set(calendars.filter((c) => !c.visible).map((c) => c.id));
|
|
2390
|
+
const dayStart = startOfDay(day);
|
|
2391
|
+
const dayEnd = addDays(dayStart, 1);
|
|
2392
|
+
const out = [];
|
|
2393
|
+
for (const task of tasks) {
|
|
2394
|
+
if (hidden.has(task.calendarId)) continue;
|
|
2395
|
+
let planned = false;
|
|
2396
|
+
for (const plan of task.plans) {
|
|
2397
|
+
if (plan.end <= dayStart.getTime() || plan.start >= dayEnd.getTime()) continue;
|
|
2398
|
+
planned = true;
|
|
2399
|
+
const start = new Date(plan.start);
|
|
2400
|
+
const end = new Date(plan.end);
|
|
2401
|
+
const continuesBefore = start < dayStart;
|
|
2402
|
+
const continuesAfter = end > dayEnd;
|
|
2403
|
+
const firstDay = startOfDay(start);
|
|
2404
|
+
const lastDay = startOfDay(new Date(Math.max(plan.start, plan.end - 1)));
|
|
2405
|
+
out.push({
|
|
2406
|
+
key: `${task.id}@${instantKey(start)}`,
|
|
2407
|
+
event: taskEvent(task, plan.start, plan.end),
|
|
2408
|
+
task,
|
|
2409
|
+
start,
|
|
2410
|
+
end,
|
|
2411
|
+
startMin: continuesBefore ? 0 : minutesOfDay(start),
|
|
2412
|
+
endMin: end >= dayEnd ? MINUTES_PER_DAY : minutesOfDay(end),
|
|
2413
|
+
continuesBefore,
|
|
2414
|
+
continuesAfter,
|
|
2415
|
+
dayIndex: daysBetween(firstDay, dayStart) + 1,
|
|
2416
|
+
dayCount: daysBetween(firstDay, lastDay) + 1,
|
|
2417
|
+
done: task.done,
|
|
2418
|
+
lane: 0
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
if (planned || task.dueAllDay) continue;
|
|
2422
|
+
if (task.due < dayStart.getTime() || task.due >= dayEnd.getTime()) continue;
|
|
2423
|
+
const due = new Date(task.due);
|
|
2424
|
+
out.push({
|
|
2425
|
+
key: `${task.id}@${instantKey(due)}`,
|
|
2426
|
+
event: taskEvent(task, task.due, task.due),
|
|
2427
|
+
task,
|
|
2428
|
+
start: due,
|
|
2429
|
+
end: due,
|
|
2430
|
+
startMin: minutesOfDay(due),
|
|
2431
|
+
endMin: minutesOfDay(due),
|
|
2432
|
+
continuesBefore: false,
|
|
2433
|
+
continuesAfter: false,
|
|
2434
|
+
dayIndex: 1,
|
|
2435
|
+
dayCount: 1,
|
|
2436
|
+
done: task.done,
|
|
2437
|
+
lane: 0
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
out.sort((a, b) => a.start.getTime() - b.start.getTime() || a.end.getTime() - b.end.getTime());
|
|
2441
|
+
return out;
|
|
2322
2442
|
}
|
|
2323
2443
|
var DEFAULT_WAKING = { from: 8 * 60, to: 22 * 60 };
|
|
2324
2444
|
function freeGaps(occurrences, minMinutes, from, to) {
|
|
@@ -2342,13 +2462,15 @@ function dayLoad(occurrences, waking) {
|
|
|
2342
2462
|
}
|
|
2343
2463
|
var FIELD_PENALTY = { location: 30, notes: 45 };
|
|
2344
2464
|
var penalise = (score, cost) => score === null ? null : score - cost;
|
|
2345
|
-
function
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2465
|
+
function scoreEventBy(event, query) {
|
|
2466
|
+
let best = null;
|
|
2467
|
+
const consider = (field, score) => {
|
|
2468
|
+
if (score !== null && (best === null || score > best.score)) best = { field, score };
|
|
2469
|
+
};
|
|
2470
|
+
consider("title", fuzzyScore(event.title, query));
|
|
2471
|
+
consider("location", penalise(fuzzyScore(event.location, query), FIELD_PENALTY.location));
|
|
2472
|
+
consider("notes", penalise(fuzzyScore(event.notes, query), FIELD_PENALTY.notes));
|
|
2473
|
+
return best;
|
|
2352
2474
|
}
|
|
2353
2475
|
var PALETTE = [
|
|
2354
2476
|
"#7ce3d8",
|
|
@@ -2377,505 +2499,110 @@ function spanLabel(occ) {
|
|
|
2377
2499
|
return occ.dayCount > 1 ? `Day ${occ.dayIndex}/${occ.dayCount}` : null;
|
|
2378
2500
|
}
|
|
2379
2501
|
|
|
2380
|
-
// src/lib/
|
|
2381
|
-
var
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2502
|
+
// src/lib/store.ts
|
|
2503
|
+
var import_react2 = __toESM(require_react(), 1);
|
|
2504
|
+
|
|
2505
|
+
// src/lib/tasks.ts
|
|
2506
|
+
var ESTIMATE_MAX = 24 * 60;
|
|
2507
|
+
var PLANS_MAX = 32;
|
|
2508
|
+
function readEstimate(value) {
|
|
2509
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
2510
|
+
return Math.min(ESTIMATE_MAX, Math.max(0, Math.round(value)));
|
|
2511
|
+
}
|
|
2512
|
+
function readPriority(value) {
|
|
2513
|
+
return value === 1 || value === 2 ? value : 0;
|
|
2514
|
+
}
|
|
2515
|
+
function readPlans(value) {
|
|
2516
|
+
if (!Array.isArray(value)) return [];
|
|
2517
|
+
const plans = [];
|
|
2518
|
+
for (const entry of value) {
|
|
2519
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
2520
|
+
const { start, end } = entry;
|
|
2521
|
+
if (typeof start !== "number" || typeof end !== "number") continue;
|
|
2522
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) continue;
|
|
2523
|
+
if (end <= start) continue;
|
|
2524
|
+
plans.push({ start, end });
|
|
2525
|
+
if (plans.length === PLANS_MAX) break;
|
|
2387
2526
|
}
|
|
2388
|
-
return
|
|
2527
|
+
return plans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
2528
|
+
}
|
|
2529
|
+
var DUE_SOON_DAYS = 7;
|
|
2530
|
+
function taskState(task, now) {
|
|
2531
|
+
if (task.done) return "done";
|
|
2532
|
+
const today = startOfDay(now);
|
|
2533
|
+
if (task.dueAllDay) {
|
|
2534
|
+
const dueDay = startOfDay(new Date(task.due));
|
|
2535
|
+
if (dueDay < today) return "overdue";
|
|
2536
|
+
return dueDay < addDays(today, DUE_SOON_DAYS + 1) ? "due-soon" : "later";
|
|
2537
|
+
}
|
|
2538
|
+
if (task.due <= now.getTime()) return "overdue";
|
|
2539
|
+
return startOfDay(new Date(task.due)) < addDays(today, DUE_SOON_DAYS + 1) ? "due-soon" : "later";
|
|
2540
|
+
}
|
|
2541
|
+
var NOTES_PENALTY = 45;
|
|
2542
|
+
function scoreTaskBy(task, query) {
|
|
2543
|
+
const title = fuzzyScore(task.title, query);
|
|
2544
|
+
const notes = fuzzyScore(task.notes, query);
|
|
2545
|
+
const penalised = notes === null ? null : notes - NOTES_PENALTY;
|
|
2546
|
+
let best = title === null ? null : { field: "title", score: title };
|
|
2547
|
+
if (penalised !== null && (best === null || penalised > best.score)) {
|
|
2548
|
+
best = { field: "notes", score: penalised };
|
|
2549
|
+
}
|
|
2550
|
+
return best;
|
|
2551
|
+
}
|
|
2552
|
+
function compareTasks(a, b) {
|
|
2553
|
+
return a.due - b.due || b.priority - a.priority || a.title.localeCompare(b.title);
|
|
2389
2554
|
}
|
|
2390
2555
|
|
|
2391
|
-
// src/lib/places.ts
|
|
2392
|
-
var COORDS = {
|
|
2393
|
-
"Africa/Abidjan": [5.32, -4.03],
|
|
2394
|
-
"Africa/Accra": [5.55, -0.22],
|
|
2395
|
-
"Africa/Addis_Ababa": [9.03, 38.7],
|
|
2396
|
-
"Africa/Algiers": [36.78, 3.05],
|
|
2397
|
-
"Africa/Asmara": [15.33, 38.88],
|
|
2398
|
-
"Africa/Bamako": [12.65, -8],
|
|
2399
|
-
"Africa/Bangui": [4.37, 18.58],
|
|
2400
|
-
"Africa/Banjul": [13.47, -16.65],
|
|
2401
|
-
"Africa/Bissau": [11.85, -15.58],
|
|
2402
|
-
"Africa/Blantyre": [-15.78, 35],
|
|
2403
|
-
"Africa/Brazzaville": [-4.27, 15.28],
|
|
2404
|
-
"Africa/Bujumbura": [-3.38, 29.37],
|
|
2405
|
-
"Africa/Cairo": [30.05, 31.25],
|
|
2406
|
-
"Africa/Casablanca": [33.65, -7.58],
|
|
2407
|
-
"Africa/Ceuta": [35.88, -5.32],
|
|
2408
|
-
"Africa/Conakry": [9.52, -13.72],
|
|
2409
|
-
"Africa/Dakar": [14.67, -17.43],
|
|
2410
|
-
"Africa/Dar_es_Salaam": [-6.8, 39.28],
|
|
2411
|
-
"Africa/Djibouti": [11.6, 43.15],
|
|
2412
|
-
"Africa/Douala": [4.05, 9.7],
|
|
2413
|
-
"Africa/El_Aaiun": [27.15, -13.2],
|
|
2414
|
-
"Africa/Freetown": [8.5, -13.25],
|
|
2415
|
-
"Africa/Gaborone": [-24.65, 25.92],
|
|
2416
|
-
"Africa/Harare": [-17.83, 31.05],
|
|
2417
|
-
"Africa/Johannesburg": [-26.25, 28],
|
|
2418
|
-
"Africa/Juba": [4.85, 31.62],
|
|
2419
|
-
"Africa/Kampala": [0.32, 32.42],
|
|
2420
|
-
"Africa/Khartoum": [15.6, 32.53],
|
|
2421
|
-
"Africa/Kigali": [-1.95, 30.07],
|
|
2422
|
-
"Africa/Kinshasa": [-4.3, 15.3],
|
|
2423
|
-
"Africa/Lagos": [6.45, 3.4],
|
|
2424
|
-
"Africa/Libreville": [0.38, 9.45],
|
|
2425
|
-
"Africa/Lome": [6.13, 1.22],
|
|
2426
|
-
"Africa/Luanda": [-8.8, 13.23],
|
|
2427
|
-
"Africa/Lubumbashi": [-11.67, 27.47],
|
|
2428
|
-
"Africa/Lusaka": [-15.42, 28.28],
|
|
2429
|
-
"Africa/Malabo": [3.75, 8.78],
|
|
2430
|
-
"Africa/Maputo": [-25.97, 32.58],
|
|
2431
|
-
"Africa/Maseru": [-29.47, 27.5],
|
|
2432
|
-
"Africa/Mbabane": [-26.3, 31.1],
|
|
2433
|
-
"Africa/Mogadishu": [2.07, 45.37],
|
|
2434
|
-
"Africa/Monrovia": [6.3, -10.78],
|
|
2435
|
-
"Africa/Nairobi": [-1.28, 36.82],
|
|
2436
|
-
"Africa/Ndjamena": [12.12, 15.05],
|
|
2437
|
-
"Africa/Niamey": [13.52, 2.12],
|
|
2438
|
-
"Africa/Nouakchott": [18.1, -15.95],
|
|
2439
|
-
"Africa/Ouagadougou": [12.37, -1.52],
|
|
2440
|
-
"Africa/Porto-Novo": [6.48, 2.62],
|
|
2441
|
-
"Africa/Sao_Tome": [0.33, 6.73],
|
|
2442
|
-
"Africa/Tripoli": [32.9, 13.18],
|
|
2443
|
-
"Africa/Tunis": [36.8, 10.18],
|
|
2444
|
-
"Africa/Windhoek": [-22.57, 17.1],
|
|
2445
|
-
"America/Adak": [51.88, -176.66],
|
|
2446
|
-
"America/Anchorage": [61.22, -149.9],
|
|
2447
|
-
"America/Anguilla": [18.2, -63.07],
|
|
2448
|
-
"America/Antigua": [17.05, -61.8],
|
|
2449
|
-
"America/Araguaina": [-7.2, -48.2],
|
|
2450
|
-
"America/Argentina/Buenos_Aires": [-34.6, -58.45],
|
|
2451
|
-
"America/Argentina/Catamarca": [-28.47, -65.78],
|
|
2452
|
-
"America/Argentina/Cordoba": [-31.4, -64.18],
|
|
2453
|
-
"America/Argentina/Jujuy": [-24.18, -65.3],
|
|
2454
|
-
"America/Argentina/La_Rioja": [-29.43, -66.85],
|
|
2455
|
-
"America/Argentina/Mendoza": [-32.88, -68.82],
|
|
2456
|
-
"America/Argentina/Rio_Gallegos": [-51.63, -69.22],
|
|
2457
|
-
"America/Argentina/Salta": [-24.78, -65.42],
|
|
2458
|
-
"America/Argentina/San_Juan": [-31.53, -68.52],
|
|
2459
|
-
"America/Argentina/San_Luis": [-33.32, -66.35],
|
|
2460
|
-
"America/Argentina/Tucuman": [-26.82, -65.22],
|
|
2461
|
-
"America/Argentina/Ushuaia": [-54.8, -68.3],
|
|
2462
|
-
"America/Aruba": [12.5, -69.97],
|
|
2463
|
-
"America/Asuncion": [-25.27, -57.67],
|
|
2464
|
-
"America/Atikokan": [48.76, -91.62],
|
|
2465
|
-
"America/Bahia": [-12.98, -38.52],
|
|
2466
|
-
"America/Bahia_Banderas": [20.8, -105.25],
|
|
2467
|
-
"America/Barbados": [13.1, -59.62],
|
|
2468
|
-
"America/Belem": [-1.45, -48.48],
|
|
2469
|
-
"America/Belize": [17.5, -88.2],
|
|
2470
|
-
"America/Blanc-Sablon": [51.42, -57.12],
|
|
2471
|
-
"America/Boa_Vista": [2.82, -60.67],
|
|
2472
|
-
"America/Bogota": [4.6, -74.08],
|
|
2473
|
-
"America/Boise": [43.61, -116.2],
|
|
2474
|
-
"America/Cambridge_Bay": [69.11, -105.05],
|
|
2475
|
-
"America/Campo_Grande": [-20.45, -54.62],
|
|
2476
|
-
"America/Cancun": [21.08, -86.77],
|
|
2477
|
-
"America/Caracas": [10.5, -66.93],
|
|
2478
|
-
"America/Cayenne": [4.93, -52.33],
|
|
2479
|
-
"America/Cayman": [19.3, -81.38],
|
|
2480
|
-
"America/Chicago": [41.85, -87.65],
|
|
2481
|
-
"America/Chihuahua": [28.63, -106.08],
|
|
2482
|
-
"America/Ciudad_Juarez": [31.73, -106.48],
|
|
2483
|
-
"America/Costa_Rica": [9.93, -84.08],
|
|
2484
|
-
"America/Coyhaique": [-45.57, -72.07],
|
|
2485
|
-
"America/Creston": [49.1, -116.52],
|
|
2486
|
-
"America/Cuiaba": [-15.58, -56.08],
|
|
2487
|
-
"America/Curacao": [12.18, -69],
|
|
2488
|
-
"America/Danmarkshavn": [76.77, -18.67],
|
|
2489
|
-
"America/Dawson": [64.07, -139.42],
|
|
2490
|
-
"America/Dawson_Creek": [55.77, -120.23],
|
|
2491
|
-
"America/Denver": [39.74, -104.98],
|
|
2492
|
-
"America/Detroit": [42.33, -83.05],
|
|
2493
|
-
"America/Dominica": [15.3, -61.4],
|
|
2494
|
-
"America/Edmonton": [53.55, -113.47],
|
|
2495
|
-
"America/Eirunepe": [-6.67, -69.87],
|
|
2496
|
-
"America/El_Salvador": [13.7, -89.2],
|
|
2497
|
-
"America/Fort_Nelson": [58.8, -122.7],
|
|
2498
|
-
"America/Fortaleza": [-3.72, -38.5],
|
|
2499
|
-
"America/Glace_Bay": [46.2, -59.95],
|
|
2500
|
-
"America/Goose_Bay": [53.33, -60.42],
|
|
2501
|
-
"America/Grand_Turk": [21.47, -71.13],
|
|
2502
|
-
"America/Grenada": [12.05, -61.75],
|
|
2503
|
-
"America/Guadeloupe": [16.23, -61.53],
|
|
2504
|
-
"America/Guatemala": [14.63, -90.52],
|
|
2505
|
-
"America/Guayaquil": [-2.17, -79.83],
|
|
2506
|
-
"America/Guyana": [6.8, -58.17],
|
|
2507
|
-
"America/Halifax": [44.65, -63.6],
|
|
2508
|
-
"America/Havana": [23.13, -82.37],
|
|
2509
|
-
"America/Hermosillo": [29.07, -110.97],
|
|
2510
|
-
"America/Indiana/Indianapolis": [39.77, -86.16],
|
|
2511
|
-
"America/Indiana/Knox": [41.3, -86.62],
|
|
2512
|
-
"America/Indiana/Marengo": [38.38, -86.34],
|
|
2513
|
-
"America/Indiana/Petersburg": [38.49, -87.28],
|
|
2514
|
-
"America/Indiana/Tell_City": [37.95, -86.76],
|
|
2515
|
-
"America/Indiana/Vevay": [38.75, -85.07],
|
|
2516
|
-
"America/Indiana/Vincennes": [38.68, -87.53],
|
|
2517
|
-
"America/Indiana/Winamac": [41.05, -86.6],
|
|
2518
|
-
"America/Inuvik": [68.35, -133.72],
|
|
2519
|
-
"America/Iqaluit": [63.73, -68.47],
|
|
2520
|
-
"America/Jamaica": [17.97, -76.79],
|
|
2521
|
-
"America/Juneau": [58.3, -134.42],
|
|
2522
|
-
"America/Kentucky/Louisville": [38.25, -85.76],
|
|
2523
|
-
"America/Kentucky/Monticello": [36.83, -84.85],
|
|
2524
|
-
"America/Kralendijk": [12.15, -68.28],
|
|
2525
|
-
"America/La_Paz": [-16.5, -68.15],
|
|
2526
|
-
"America/Lima": [-12.05, -77.05],
|
|
2527
|
-
"America/Los_Angeles": [34.05, -118.24],
|
|
2528
|
-
"America/Lower_Princes": [18.05, -63.05],
|
|
2529
|
-
"America/Maceio": [-9.67, -35.72],
|
|
2530
|
-
"America/Managua": [12.15, -86.28],
|
|
2531
|
-
"America/Manaus": [-3.13, -60.02],
|
|
2532
|
-
"America/Marigot": [18.07, -63.08],
|
|
2533
|
-
"America/Martinique": [14.6, -61.08],
|
|
2534
|
-
"America/Matamoros": [25.83, -97.5],
|
|
2535
|
-
"America/Mazatlan": [23.22, -106.42],
|
|
2536
|
-
"America/Menominee": [45.11, -87.61],
|
|
2537
|
-
"America/Merida": [20.97, -89.62],
|
|
2538
|
-
"America/Metlakatla": [55.13, -131.58],
|
|
2539
|
-
"America/Mexico_City": [19.4, -99.15],
|
|
2540
|
-
"America/Miquelon": [47.05, -56.33],
|
|
2541
|
-
"America/Moncton": [46.1, -64.78],
|
|
2542
|
-
"America/Monterrey": [25.67, -100.32],
|
|
2543
|
-
"America/Montevideo": [-34.91, -56.21],
|
|
2544
|
-
"America/Montserrat": [16.72, -62.22],
|
|
2545
|
-
"America/Nassau": [25.08, -77.35],
|
|
2546
|
-
"America/New_York": [40.71, -74.01],
|
|
2547
|
-
"America/Nome": [64.5, -165.41],
|
|
2548
|
-
"America/Noronha": [-3.85, -32.42],
|
|
2549
|
-
"America/North_Dakota/Beulah": [47.26, -101.78],
|
|
2550
|
-
"America/North_Dakota/Center": [47.12, -101.3],
|
|
2551
|
-
"America/North_Dakota/New_Salem": [46.84, -101.41],
|
|
2552
|
-
"America/Nuuk": [64.18, -51.73],
|
|
2553
|
-
"America/Ojinaga": [29.57, -104.42],
|
|
2554
|
-
"America/Panama": [8.97, -79.53],
|
|
2555
|
-
"America/Paramaribo": [5.83, -55.17],
|
|
2556
|
-
"America/Phoenix": [33.45, -112.07],
|
|
2557
|
-
"America/Port-au-Prince": [18.53, -72.33],
|
|
2558
|
-
"America/Port_of_Spain": [10.65, -61.52],
|
|
2559
|
-
"America/Porto_Velho": [-8.77, -63.9],
|
|
2560
|
-
"America/Puerto_Rico": [18.47, -66.11],
|
|
2561
|
-
"America/Punta_Arenas": [-53.15, -70.92],
|
|
2562
|
-
"America/Rankin_Inlet": [62.82, -92.08],
|
|
2563
|
-
"America/Recife": [-8.05, -34.9],
|
|
2564
|
-
"America/Regina": [50.4, -104.65],
|
|
2565
|
-
"America/Resolute": [74.7, -94.83],
|
|
2566
|
-
"America/Rio_Branco": [-9.97, -67.8],
|
|
2567
|
-
"America/Santarem": [-2.43, -54.87],
|
|
2568
|
-
"America/Santiago": [-33.45, -70.67],
|
|
2569
|
-
"America/Santo_Domingo": [18.47, -69.9],
|
|
2570
|
-
"America/Sao_Paulo": [-23.53, -46.62],
|
|
2571
|
-
"America/Scoresbysund": [70.48, -21.97],
|
|
2572
|
-
"America/Sitka": [57.18, -135.3],
|
|
2573
|
-
"America/St_Barthelemy": [17.88, -62.85],
|
|
2574
|
-
"America/St_Johns": [47.57, -52.72],
|
|
2575
|
-
"America/St_Kitts": [17.3, -62.72],
|
|
2576
|
-
"America/St_Lucia": [14.02, -61],
|
|
2577
|
-
"America/St_Thomas": [18.35, -64.93],
|
|
2578
|
-
"America/St_Vincent": [13.15, -61.23],
|
|
2579
|
-
"America/Swift_Current": [50.28, -107.83],
|
|
2580
|
-
"America/Tegucigalpa": [14.1, -87.22],
|
|
2581
|
-
"America/Thule": [76.57, -68.78],
|
|
2582
|
-
"America/Tijuana": [32.53, -117.02],
|
|
2583
|
-
"America/Toronto": [43.65, -79.38],
|
|
2584
|
-
"America/Tortola": [18.45, -64.62],
|
|
2585
|
-
"America/Vancouver": [49.27, -123.12],
|
|
2586
|
-
"America/Whitehorse": [60.72, -135.05],
|
|
2587
|
-
"America/Winnipeg": [49.88, -97.15],
|
|
2588
|
-
"America/Yakutat": [59.55, -139.73],
|
|
2589
|
-
"Antarctica/Casey": [-66.28, 110.52],
|
|
2590
|
-
"Antarctica/Davis": [-68.58, 77.97],
|
|
2591
|
-
"Antarctica/DumontDUrville": [-66.67, 140.02],
|
|
2592
|
-
"Antarctica/Macquarie": [-54.5, 158.95],
|
|
2593
|
-
"Antarctica/Mawson": [-67.6, 62.88],
|
|
2594
|
-
"Antarctica/McMurdo": [-77.83, 166.6],
|
|
2595
|
-
"Antarctica/Palmer": [-64.8, -64.1],
|
|
2596
|
-
"Antarctica/Rothera": [-67.57, -68.13],
|
|
2597
|
-
"Antarctica/Syowa": [-69.01, 39.59],
|
|
2598
|
-
"Antarctica/Troll": [-72.01, 2.53],
|
|
2599
|
-
"Antarctica/Vostok": [-78.4, 106.9],
|
|
2600
|
-
"Arctic/Longyearbyen": [78, 16],
|
|
2601
|
-
"Asia/Aden": [12.75, 45.2],
|
|
2602
|
-
"Asia/Almaty": [43.25, 76.95],
|
|
2603
|
-
"Asia/Amman": [31.95, 35.93],
|
|
2604
|
-
"Asia/Anadyr": [64.75, 177.48],
|
|
2605
|
-
"Asia/Aqtau": [44.52, 50.27],
|
|
2606
|
-
"Asia/Aqtobe": [50.28, 57.17],
|
|
2607
|
-
"Asia/Ashgabat": [37.95, 58.38],
|
|
2608
|
-
"Asia/Atyrau": [47.12, 51.93],
|
|
2609
|
-
"Asia/Baghdad": [33.35, 44.42],
|
|
2610
|
-
"Asia/Bahrain": [26.38, 50.58],
|
|
2611
|
-
"Asia/Baku": [40.38, 49.85],
|
|
2612
|
-
"Asia/Bangkok": [13.75, 100.52],
|
|
2613
|
-
"Asia/Barnaul": [53.37, 83.75],
|
|
2614
|
-
"Asia/Beirut": [33.88, 35.5],
|
|
2615
|
-
"Asia/Bishkek": [42.9, 74.6],
|
|
2616
|
-
"Asia/Brunei": [4.93, 114.92],
|
|
2617
|
-
"Asia/Chita": [52.05, 113.47],
|
|
2618
|
-
"Asia/Colombo": [6.93, 79.85],
|
|
2619
|
-
"Asia/Damascus": [33.5, 36.3],
|
|
2620
|
-
"Asia/Dhaka": [23.72, 90.42],
|
|
2621
|
-
"Asia/Dili": [-8.55, 125.58],
|
|
2622
|
-
"Asia/Dubai": [25.3, 55.3],
|
|
2623
|
-
"Asia/Dushanbe": [38.58, 68.8],
|
|
2624
|
-
"Asia/Famagusta": [35.12, 33.95],
|
|
2625
|
-
"Asia/Gaza": [31.5, 34.47],
|
|
2626
|
-
"Asia/Hebron": [31.53, 35.09],
|
|
2627
|
-
"Asia/Ho_Chi_Minh": [10.75, 106.67],
|
|
2628
|
-
"Asia/Hong_Kong": [22.28, 114.15],
|
|
2629
|
-
"Asia/Hovd": [48.02, 91.65],
|
|
2630
|
-
"Asia/Irkutsk": [52.27, 104.33],
|
|
2631
|
-
"Asia/Jakarta": [-6.17, 106.8],
|
|
2632
|
-
"Asia/Jayapura": [-2.53, 140.7],
|
|
2633
|
-
"Asia/Jerusalem": [31.78, 35.22],
|
|
2634
|
-
"Asia/Kabul": [34.52, 69.2],
|
|
2635
|
-
"Asia/Kamchatka": [53.02, 158.65],
|
|
2636
|
-
"Asia/Karachi": [24.87, 67.05],
|
|
2637
|
-
"Asia/Kathmandu": [27.72, 85.32],
|
|
2638
|
-
"Asia/Khandyga": [62.66, 135.55],
|
|
2639
|
-
"Asia/Kolkata": [22.53, 88.37],
|
|
2640
|
-
"Asia/Krasnoyarsk": [56.02, 92.83],
|
|
2641
|
-
"Asia/Kuala_Lumpur": [3.17, 101.7],
|
|
2642
|
-
"Asia/Kuching": [1.55, 110.33],
|
|
2643
|
-
"Asia/Kuwait": [29.33, 47.98],
|
|
2644
|
-
"Asia/Macau": [22.2, 113.54],
|
|
2645
|
-
"Asia/Magadan": [59.57, 150.8],
|
|
2646
|
-
"Asia/Makassar": [-5.12, 119.4],
|
|
2647
|
-
"Asia/Manila": [14.59, 120.97],
|
|
2648
|
-
"Asia/Muscat": [23.6, 58.58],
|
|
2649
|
-
"Asia/Nicosia": [35.17, 33.37],
|
|
2650
|
-
"Asia/Novokuznetsk": [53.75, 87.12],
|
|
2651
|
-
"Asia/Novosibirsk": [55.03, 82.92],
|
|
2652
|
-
"Asia/Omsk": [55, 73.4],
|
|
2653
|
-
"Asia/Oral": [51.22, 51.35],
|
|
2654
|
-
"Asia/Phnom_Penh": [11.55, 104.92],
|
|
2655
|
-
"Asia/Pontianak": [-0.03, 109.33],
|
|
2656
|
-
"Asia/Pyongyang": [39.02, 125.75],
|
|
2657
|
-
"Asia/Qatar": [25.28, 51.53],
|
|
2658
|
-
"Asia/Qostanay": [53.2, 63.62],
|
|
2659
|
-
"Asia/Qyzylorda": [44.8, 65.47],
|
|
2660
|
-
"Asia/Riyadh": [24.63, 46.72],
|
|
2661
|
-
"Asia/Sakhalin": [46.97, 142.7],
|
|
2662
|
-
"Asia/Samarkand": [39.67, 66.8],
|
|
2663
|
-
"Asia/Seoul": [37.55, 126.97],
|
|
2664
|
-
"Asia/Shanghai": [31.23, 121.47],
|
|
2665
|
-
"Asia/Singapore": [1.28, 103.85],
|
|
2666
|
-
"Asia/Srednekolymsk": [67.47, 153.72],
|
|
2667
|
-
"Asia/Taipei": [25.05, 121.5],
|
|
2668
|
-
"Asia/Tashkent": [41.33, 69.3],
|
|
2669
|
-
"Asia/Tbilisi": [41.72, 44.82],
|
|
2670
|
-
"Asia/Tehran": [35.67, 51.43],
|
|
2671
|
-
"Asia/Thimphu": [27.47, 89.65],
|
|
2672
|
-
"Asia/Tokyo": [35.65, 139.74],
|
|
2673
|
-
"Asia/Tomsk": [56.5, 84.97],
|
|
2674
|
-
"Asia/Ulaanbaatar": [47.92, 106.88],
|
|
2675
|
-
"Asia/Urumqi": [43.8, 87.58],
|
|
2676
|
-
"Asia/Ust-Nera": [64.56, 143.23],
|
|
2677
|
-
"Asia/Vientiane": [17.97, 102.6],
|
|
2678
|
-
"Asia/Vladivostok": [43.17, 131.93],
|
|
2679
|
-
"Asia/Yakutsk": [62, 129.67],
|
|
2680
|
-
"Asia/Yangon": [16.78, 96.17],
|
|
2681
|
-
"Asia/Yekaterinburg": [56.85, 60.6],
|
|
2682
|
-
"Asia/Yerevan": [40.18, 44.5],
|
|
2683
|
-
"Atlantic/Azores": [37.73, -25.67],
|
|
2684
|
-
"Atlantic/Bermuda": [32.28, -64.77],
|
|
2685
|
-
"Atlantic/Canary": [28.1, -15.4],
|
|
2686
|
-
"Atlantic/Cape_Verde": [14.92, -23.52],
|
|
2687
|
-
"Atlantic/Faroe": [62.02, -6.77],
|
|
2688
|
-
"Atlantic/Madeira": [32.63, -16.9],
|
|
2689
|
-
"Atlantic/Reykjavik": [64.15, -21.85],
|
|
2690
|
-
"Atlantic/South_Georgia": [-54.27, -36.53],
|
|
2691
|
-
"Atlantic/St_Helena": [-15.92, -5.7],
|
|
2692
|
-
"Atlantic/Stanley": [-51.7, -57.85],
|
|
2693
|
-
"Australia/Adelaide": [-34.92, 138.58],
|
|
2694
|
-
"Australia/Brisbane": [-27.47, 153.03],
|
|
2695
|
-
"Australia/Broken_Hill": [-31.95, 141.45],
|
|
2696
|
-
"Australia/Darwin": [-12.47, 130.83],
|
|
2697
|
-
"Australia/Eucla": [-31.72, 128.87],
|
|
2698
|
-
"Australia/Hobart": [-42.88, 147.32],
|
|
2699
|
-
"Australia/Lindeman": [-20.27, 149],
|
|
2700
|
-
"Australia/Lord_Howe": [-31.55, 159.08],
|
|
2701
|
-
"Australia/Melbourne": [-37.82, 144.97],
|
|
2702
|
-
"Australia/Perth": [-31.95, 115.85],
|
|
2703
|
-
"Australia/Sydney": [-33.87, 151.22],
|
|
2704
|
-
"Europe/Amsterdam": [52.37, 4.9],
|
|
2705
|
-
"Europe/Andorra": [42.5, 1.52],
|
|
2706
|
-
"Europe/Astrakhan": [46.35, 48.05],
|
|
2707
|
-
"Europe/Athens": [37.97, 23.72],
|
|
2708
|
-
"Europe/Belgrade": [44.83, 20.5],
|
|
2709
|
-
"Europe/Berlin": [52.5, 13.37],
|
|
2710
|
-
"Europe/Bratislava": [48.15, 17.12],
|
|
2711
|
-
"Europe/Brussels": [50.83, 4.33],
|
|
2712
|
-
"Europe/Bucharest": [44.43, 26.1],
|
|
2713
|
-
"Europe/Budapest": [47.5, 19.08],
|
|
2714
|
-
"Europe/Busingen": [47.7, 8.68],
|
|
2715
|
-
"Europe/Chisinau": [47, 28.83],
|
|
2716
|
-
"Europe/Copenhagen": [55.67, 12.58],
|
|
2717
|
-
"Europe/Dublin": [53.33, -6.25],
|
|
2718
|
-
"Europe/Gibraltar": [36.13, -5.35],
|
|
2719
|
-
"Europe/Guernsey": [49.45, -2.54],
|
|
2720
|
-
"Europe/Helsinki": [60.17, 24.97],
|
|
2721
|
-
"Europe/Isle_of_Man": [54.15, -4.47],
|
|
2722
|
-
"Europe/Istanbul": [41.02, 28.97],
|
|
2723
|
-
"Europe/Jersey": [49.18, -2.11],
|
|
2724
|
-
"Europe/Kaliningrad": [54.72, 20.5],
|
|
2725
|
-
"Europe/Kirov": [58.6, 49.65],
|
|
2726
|
-
"Europe/Kyiv": [50.43, 30.52],
|
|
2727
|
-
"Europe/Lisbon": [38.72, -9.13],
|
|
2728
|
-
"Europe/Ljubljana": [46.05, 14.52],
|
|
2729
|
-
"Europe/London": [51.51, -0.13],
|
|
2730
|
-
"Europe/Luxembourg": [49.6, 6.15],
|
|
2731
|
-
"Europe/Madrid": [40.4, -3.68],
|
|
2732
|
-
"Europe/Malta": [35.9, 14.52],
|
|
2733
|
-
"Europe/Mariehamn": [60.1, 19.95],
|
|
2734
|
-
"Europe/Minsk": [53.9, 27.57],
|
|
2735
|
-
"Europe/Monaco": [43.7, 7.38],
|
|
2736
|
-
"Europe/Moscow": [55.76, 37.62],
|
|
2737
|
-
"Europe/Oslo": [59.92, 10.75],
|
|
2738
|
-
"Europe/Paris": [48.87, 2.33],
|
|
2739
|
-
"Europe/Podgorica": [42.43, 19.27],
|
|
2740
|
-
"Europe/Prague": [50.08, 14.43],
|
|
2741
|
-
"Europe/Riga": [56.95, 24.1],
|
|
2742
|
-
"Europe/Rome": [41.9, 12.48],
|
|
2743
|
-
"Europe/Samara": [53.2, 50.15],
|
|
2744
|
-
"Europe/San_Marino": [43.92, 12.47],
|
|
2745
|
-
"Europe/Sarajevo": [43.87, 18.42],
|
|
2746
|
-
"Europe/Saratov": [51.57, 46.03],
|
|
2747
|
-
"Europe/Simferopol": [44.95, 34.1],
|
|
2748
|
-
"Europe/Skopje": [41.98, 21.43],
|
|
2749
|
-
"Europe/Sofia": [42.68, 23.32],
|
|
2750
|
-
"Europe/Stockholm": [59.33, 18.05],
|
|
2751
|
-
"Europe/Tallinn": [59.42, 24.75],
|
|
2752
|
-
"Europe/Tirane": [41.33, 19.83],
|
|
2753
|
-
"Europe/Ulyanovsk": [54.33, 48.4],
|
|
2754
|
-
"Europe/Vaduz": [47.15, 9.52],
|
|
2755
|
-
"Europe/Vatican": [41.9, 12.45],
|
|
2756
|
-
"Europe/Vienna": [48.22, 16.33],
|
|
2757
|
-
"Europe/Vilnius": [54.68, 25.32],
|
|
2758
|
-
"Europe/Volgograd": [48.73, 44.42],
|
|
2759
|
-
"Europe/Warsaw": [52.25, 21],
|
|
2760
|
-
"Europe/Zagreb": [45.8, 15.97],
|
|
2761
|
-
"Europe/Zurich": [47.38, 8.53],
|
|
2762
|
-
"Indian/Antananarivo": [-18.92, 47.52],
|
|
2763
|
-
"Indian/Chagos": [-7.33, 72.42],
|
|
2764
|
-
"Indian/Christmas": [-10.42, 105.72],
|
|
2765
|
-
"Indian/Cocos": [-12.17, 96.92],
|
|
2766
|
-
"Indian/Comoro": [-11.68, 43.27],
|
|
2767
|
-
"Indian/Kerguelen": [-49.35, 70.22],
|
|
2768
|
-
"Indian/Mahe": [-4.67, 55.47],
|
|
2769
|
-
"Indian/Maldives": [4.17, 73.5],
|
|
2770
|
-
"Indian/Mauritius": [-20.17, 57.5],
|
|
2771
|
-
"Indian/Mayotte": [-12.78, 45.23],
|
|
2772
|
-
"Indian/Reunion": [-20.87, 55.47],
|
|
2773
|
-
"Pacific/Apia": [-13.83, -171.73],
|
|
2774
|
-
"Pacific/Auckland": [-36.87, 174.77],
|
|
2775
|
-
"Pacific/Bougainville": [-6.22, 155.57],
|
|
2776
|
-
"Pacific/Chatham": [-43.95, -176.55],
|
|
2777
|
-
"Pacific/Chuuk": [7.42, 151.78],
|
|
2778
|
-
"Pacific/Easter": [-27.15, -109.43],
|
|
2779
|
-
"Pacific/Efate": [-17.67, 168.42],
|
|
2780
|
-
"Pacific/Fakaofo": [-9.37, -171.23],
|
|
2781
|
-
"Pacific/Fiji": [-18.13, 178.42],
|
|
2782
|
-
"Pacific/Funafuti": [-8.52, 179.22],
|
|
2783
|
-
"Pacific/Galapagos": [-0.9, -89.6],
|
|
2784
|
-
"Pacific/Gambier": [-23.13, -134.95],
|
|
2785
|
-
"Pacific/Guadalcanal": [-9.53, 160.2],
|
|
2786
|
-
"Pacific/Guam": [13.47, 144.75],
|
|
2787
|
-
"Pacific/Honolulu": [21.31, -157.86],
|
|
2788
|
-
"Pacific/Kanton": [-2.78, -171.72],
|
|
2789
|
-
"Pacific/Kiritimati": [1.87, -157.33],
|
|
2790
|
-
"Pacific/Kosrae": [5.32, 162.98],
|
|
2791
|
-
"Pacific/Kwajalein": [9.08, 167.33],
|
|
2792
|
-
"Pacific/Majuro": [7.15, 171.2],
|
|
2793
|
-
"Pacific/Marquesas": [-9, -139.5],
|
|
2794
|
-
"Pacific/Midway": [28.22, -177.37],
|
|
2795
|
-
"Pacific/Nauru": [-0.52, 166.92],
|
|
2796
|
-
"Pacific/Niue": [-19.02, -169.92],
|
|
2797
|
-
"Pacific/Norfolk": [-29.05, 167.97],
|
|
2798
|
-
"Pacific/Noumea": [-22.27, 166.45],
|
|
2799
|
-
"Pacific/Pago_Pago": [-14.27, -170.7],
|
|
2800
|
-
"Pacific/Palau": [7.33, 134.48],
|
|
2801
|
-
"Pacific/Pitcairn": [-25.07, -130.08],
|
|
2802
|
-
"Pacific/Pohnpei": [6.97, 158.22],
|
|
2803
|
-
"Pacific/Port_Moresby": [-9.5, 147.17],
|
|
2804
|
-
"Pacific/Rarotonga": [-21.23, -159.77],
|
|
2805
|
-
"Pacific/Saipan": [15.2, 145.75],
|
|
2806
|
-
"Pacific/Tahiti": [-17.53, -149.57],
|
|
2807
|
-
"Pacific/Tarawa": [1.42, 173],
|
|
2808
|
-
"Pacific/Tongatapu": [-21.13, -175.2],
|
|
2809
|
-
"Pacific/Wake": [19.28, 166.62],
|
|
2810
|
-
"Pacific/Wallis": [-13.3, -176.17]
|
|
2811
|
-
};
|
|
2812
|
-
var ALIASES = {
|
|
2813
|
-
"Africa/Asmera": "Africa/Asmara",
|
|
2814
|
-
"America/Buenos_Aires": "America/Argentina/Buenos_Aires",
|
|
2815
|
-
"America/Catamarca": "America/Argentina/Catamarca",
|
|
2816
|
-
"America/Coral_Harbour": "America/Atikokan",
|
|
2817
|
-
"America/Cordoba": "America/Argentina/Cordoba",
|
|
2818
|
-
"America/Godthab": "America/Nuuk",
|
|
2819
|
-
"America/Indianapolis": "America/Indiana/Indianapolis",
|
|
2820
|
-
"America/Jujuy": "America/Argentina/Jujuy",
|
|
2821
|
-
"America/Louisville": "America/Kentucky/Louisville",
|
|
2822
|
-
"America/Mendoza": "America/Argentina/Mendoza",
|
|
2823
|
-
"Asia/Calcutta": "Asia/Kolkata",
|
|
2824
|
-
"Asia/Katmandu": "Asia/Kathmandu",
|
|
2825
|
-
"Asia/Rangoon": "Asia/Yangon",
|
|
2826
|
-
"Asia/Saigon": "Asia/Ho_Chi_Minh",
|
|
2827
|
-
"Atlantic/Faeroe": "Atlantic/Faroe",
|
|
2828
|
-
"Europe/Kiev": "Europe/Kyiv",
|
|
2829
|
-
"Pacific/Enderbury": "Pacific/Kanton",
|
|
2830
|
-
"Pacific/Ponape": "Pacific/Pohnpei",
|
|
2831
|
-
"Pacific/Truk": "Pacific/Chuuk"
|
|
2832
|
-
};
|
|
2833
|
-
function placeForZone(zone) {
|
|
2834
|
-
const coords = COORDS[zone] ?? COORDS[ALIASES[zone]];
|
|
2835
|
-
if (!coords) return null;
|
|
2836
|
-
return { zone, lat: coords[0], lon: coords[1], label: zoneLabel(zone) };
|
|
2837
|
-
}
|
|
2838
|
-
var PLACE_OPTIONS = Object.keys(COORDS).map((zone) => ({ value: zone, label: zoneLabel(zone) })).sort((a, b) => a.label.localeCompare(b.label));
|
|
2839
|
-
|
|
2840
|
-
// src/lib/store.ts
|
|
2841
|
-
var import_react2 = __toESM(require_react(), 1);
|
|
2842
|
-
|
|
2843
|
-
// src/lib/notify.ts
|
|
2844
|
-
var import_react = __toESM(require_react(), 1);
|
|
2845
|
-
|
|
2846
2556
|
// 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);
|
|
2557
|
+
function applyUpdate(events, id, patch, now) {
|
|
2558
|
+
return events.map((e) => e.id === id ? { ...e, ...patch, updatedAt: now } : e);
|
|
2852
2559
|
}
|
|
2853
|
-
function applyRemove(events, id) {
|
|
2854
|
-
|
|
2560
|
+
function applyRemove(events, id, now) {
|
|
2561
|
+
const buried = events.filter((e) => e.id === id || e.seriesId === id);
|
|
2562
|
+
return {
|
|
2563
|
+
events: events.filter((e) => e.id !== id && e.seriesId !== id),
|
|
2564
|
+
graves: buried.map((e) => ({ id: e.id, at: now }))
|
|
2565
|
+
};
|
|
2855
2566
|
}
|
|
2856
|
-
function applySplit(events, id, at, tail, mintId) {
|
|
2567
|
+
function applySplit(events, id, at, tail, mintId, now) {
|
|
2857
2568
|
const original = events.find((e) => e.id === id);
|
|
2858
2569
|
if (!original) return { events, created: null };
|
|
2859
2570
|
const split = splitRule(original, original.start, at);
|
|
2860
|
-
const created = {
|
|
2571
|
+
const created = {
|
|
2572
|
+
...tail,
|
|
2573
|
+
...split.tail,
|
|
2574
|
+
seriesId: "",
|
|
2575
|
+
seriesKey: "",
|
|
2576
|
+
id: mintId(),
|
|
2577
|
+
updatedAt: now
|
|
2578
|
+
};
|
|
2861
2579
|
const seed = new Date(original.start);
|
|
2862
2580
|
const detached = (e) => e.seriesId === id && (keyToInstant(e.seriesKey, seed) ?? -Infinity) >= at;
|
|
2863
2581
|
return {
|
|
2864
2582
|
events: [
|
|
2583
|
+
// The head, the tail and every re-pointed override are the three things
|
|
2584
|
+
// this touches, so they are the three it stamps. An override before the
|
|
2585
|
+
// cut is handed back as the same object and keeps the stamp it had.
|
|
2865
2586
|
...events.map(
|
|
2866
|
-
(e) => e.id === id ? { ...e, ...split.head } : detached(e) ? { ...e, seriesId: created.id } : e
|
|
2587
|
+
(e) => e.id === id ? { ...e, ...split.head, updatedAt: now } : detached(e) ? { ...e, seriesId: created.id, updatedAt: now } : e
|
|
2867
2588
|
),
|
|
2868
2589
|
created
|
|
2869
2590
|
],
|
|
2870
2591
|
created
|
|
2871
2592
|
};
|
|
2872
2593
|
}
|
|
2873
|
-
function applyTruncate(events, id, at) {
|
|
2594
|
+
function applyTruncate(events, id, at, now) {
|
|
2874
2595
|
const original = events.find((e) => e.id === id);
|
|
2875
|
-
if (!original) return events;
|
|
2596
|
+
if (!original) return { events, graves: [] };
|
|
2876
2597
|
const split = splitRule(original, original.start, at);
|
|
2877
2598
|
const seed = new Date(original.start);
|
|
2878
|
-
|
|
2599
|
+
const past = (e) => e.seriesId === id && (keyToInstant(e.seriesKey, seed) ?? -Infinity) >= at;
|
|
2600
|
+
return {
|
|
2601
|
+
events: events.filter((e) => !past(e)).map((e) => e.id === id ? { ...e, ...split.head, updatedAt: now } : e),
|
|
2602
|
+
// Only what is past the cut: the head survives the truncation, so a grave
|
|
2603
|
+
// for it would delete the series a merge is meant to keep.
|
|
2604
|
+
graves: events.filter(past).map((e) => ({ id: e.id, at: now }))
|
|
2605
|
+
};
|
|
2879
2606
|
}
|
|
2880
2607
|
|
|
2881
2608
|
// src/lib/quickadd.ts
|
|
@@ -2906,6 +2633,16 @@ function parseDate(raw, reference) {
|
|
|
2906
2633
|
const delta = (weekdayIndex - base.getDay() + 7) % 7 || 7;
|
|
2907
2634
|
return addDays(base, delta);
|
|
2908
2635
|
}
|
|
2636
|
+
const lastWeekday = /^last\s+([a-z]+)$/.exec(query);
|
|
2637
|
+
if (lastWeekday) {
|
|
2638
|
+
const idx = WEEKDAYS_LOWER.findIndex(
|
|
2639
|
+
(w) => w === lastWeekday[1] || w.slice(0, 3) === lastWeekday[1]
|
|
2640
|
+
);
|
|
2641
|
+
if (idx !== -1) {
|
|
2642
|
+
const delta = (base.getDay() - idx + 7) % 7 || 7;
|
|
2643
|
+
return addDays(base, -delta);
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2909
2646
|
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(query);
|
|
2910
2647
|
if (iso) {
|
|
2911
2648
|
const year = Number(iso[1]);
|
|
@@ -2919,11 +2656,23 @@ function parseDate(raw, reference) {
|
|
|
2919
2656
|
const resolveMonthDay = (monthStr, domStr) => {
|
|
2920
2657
|
const monthIndex = MONTHS_LOWER.findIndex((m) => m === monthStr || m.slice(0, 3) === monthStr);
|
|
2921
2658
|
const dom = Number(domStr);
|
|
2922
|
-
if (monthIndex === -1 || dom < 1
|
|
2923
|
-
return null;
|
|
2659
|
+
if (monthIndex === -1 || dom < 1) return null;
|
|
2924
2660
|
const year = reference.getFullYear();
|
|
2925
|
-
|
|
2926
|
-
|
|
2661
|
+
let best = null;
|
|
2662
|
+
for (const y of [year - 1, year, year + 1]) {
|
|
2663
|
+
if (dom > daysInMonth(y, monthIndex)) continue;
|
|
2664
|
+
const candidate = new Date(y, monthIndex, dom);
|
|
2665
|
+
if (best === null) {
|
|
2666
|
+
best = candidate;
|
|
2667
|
+
continue;
|
|
2668
|
+
}
|
|
2669
|
+
const kept = Math.abs(best.getTime() - base.getTime());
|
|
2670
|
+
const offered = Math.abs(candidate.getTime() - base.getTime());
|
|
2671
|
+
if (offered < kept || offered === kept && candidate.getTime() > base.getTime()) {
|
|
2672
|
+
best = candidate;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
return best;
|
|
2927
2676
|
};
|
|
2928
2677
|
const monthDay = /^([a-z]+)\s+(\d{1,2})$/.exec(query);
|
|
2929
2678
|
if (monthDay) {
|
|
@@ -3164,9 +2913,474 @@ function parseQuickAdd(input, reference, now = /* @__PURE__ */ new Date(), fallb
|
|
|
3164
2913
|
repeat
|
|
3165
2914
|
};
|
|
3166
2915
|
}
|
|
2916
|
+
var EDIT_FILLER = /* @__PURE__ */ new Set([...FILLER, "to", "by"]);
|
|
2917
|
+
|
|
2918
|
+
// src/lib/remind.ts
|
|
2919
|
+
function remindLabel(minutes) {
|
|
2920
|
+
return minutes === 0 ? "As it starts" : `${formatDuration(minutes)} before`;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// src/lib/theme.ts
|
|
2924
|
+
var THEMES = ["midnight", "ember", "paper"];
|
|
2925
|
+
var DEFAULT_THEME = "midnight";
|
|
2926
|
+
function isThemeName(value) {
|
|
2927
|
+
return typeof value === "string" && THEMES.includes(value);
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// src/lib/places.ts
|
|
2931
|
+
var COORDS = {
|
|
2932
|
+
"Africa/Abidjan": [5.32, -4.03],
|
|
2933
|
+
"Africa/Accra": [5.55, -0.22],
|
|
2934
|
+
"Africa/Addis_Ababa": [9.03, 38.7],
|
|
2935
|
+
"Africa/Algiers": [36.78, 3.05],
|
|
2936
|
+
"Africa/Asmara": [15.33, 38.88],
|
|
2937
|
+
"Africa/Bamako": [12.65, -8],
|
|
2938
|
+
"Africa/Bangui": [4.37, 18.58],
|
|
2939
|
+
"Africa/Banjul": [13.47, -16.65],
|
|
2940
|
+
"Africa/Bissau": [11.85, -15.58],
|
|
2941
|
+
"Africa/Blantyre": [-15.78, 35],
|
|
2942
|
+
"Africa/Brazzaville": [-4.27, 15.28],
|
|
2943
|
+
"Africa/Bujumbura": [-3.38, 29.37],
|
|
2944
|
+
"Africa/Cairo": [30.05, 31.25],
|
|
2945
|
+
"Africa/Casablanca": [33.65, -7.58],
|
|
2946
|
+
"Africa/Ceuta": [35.88, -5.32],
|
|
2947
|
+
"Africa/Conakry": [9.52, -13.72],
|
|
2948
|
+
"Africa/Dakar": [14.67, -17.43],
|
|
2949
|
+
"Africa/Dar_es_Salaam": [-6.8, 39.28],
|
|
2950
|
+
"Africa/Djibouti": [11.6, 43.15],
|
|
2951
|
+
"Africa/Douala": [4.05, 9.7],
|
|
2952
|
+
"Africa/El_Aaiun": [27.15, -13.2],
|
|
2953
|
+
"Africa/Freetown": [8.5, -13.25],
|
|
2954
|
+
"Africa/Gaborone": [-24.65, 25.92],
|
|
2955
|
+
"Africa/Harare": [-17.83, 31.05],
|
|
2956
|
+
"Africa/Johannesburg": [-26.25, 28],
|
|
2957
|
+
"Africa/Juba": [4.85, 31.62],
|
|
2958
|
+
"Africa/Kampala": [0.32, 32.42],
|
|
2959
|
+
"Africa/Khartoum": [15.6, 32.53],
|
|
2960
|
+
"Africa/Kigali": [-1.95, 30.07],
|
|
2961
|
+
"Africa/Kinshasa": [-4.3, 15.3],
|
|
2962
|
+
"Africa/Lagos": [6.45, 3.4],
|
|
2963
|
+
"Africa/Libreville": [0.38, 9.45],
|
|
2964
|
+
"Africa/Lome": [6.13, 1.22],
|
|
2965
|
+
"Africa/Luanda": [-8.8, 13.23],
|
|
2966
|
+
"Africa/Lubumbashi": [-11.67, 27.47],
|
|
2967
|
+
"Africa/Lusaka": [-15.42, 28.28],
|
|
2968
|
+
"Africa/Malabo": [3.75, 8.78],
|
|
2969
|
+
"Africa/Maputo": [-25.97, 32.58],
|
|
2970
|
+
"Africa/Maseru": [-29.47, 27.5],
|
|
2971
|
+
"Africa/Mbabane": [-26.3, 31.1],
|
|
2972
|
+
"Africa/Mogadishu": [2.07, 45.37],
|
|
2973
|
+
"Africa/Monrovia": [6.3, -10.78],
|
|
2974
|
+
"Africa/Nairobi": [-1.28, 36.82],
|
|
2975
|
+
"Africa/Ndjamena": [12.12, 15.05],
|
|
2976
|
+
"Africa/Niamey": [13.52, 2.12],
|
|
2977
|
+
"Africa/Nouakchott": [18.1, -15.95],
|
|
2978
|
+
"Africa/Ouagadougou": [12.37, -1.52],
|
|
2979
|
+
"Africa/Porto-Novo": [6.48, 2.62],
|
|
2980
|
+
"Africa/Sao_Tome": [0.33, 6.73],
|
|
2981
|
+
"Africa/Tripoli": [32.9, 13.18],
|
|
2982
|
+
"Africa/Tunis": [36.8, 10.18],
|
|
2983
|
+
"Africa/Windhoek": [-22.57, 17.1],
|
|
2984
|
+
"America/Adak": [51.88, -176.66],
|
|
2985
|
+
"America/Anchorage": [61.22, -149.9],
|
|
2986
|
+
"America/Anguilla": [18.2, -63.07],
|
|
2987
|
+
"America/Antigua": [17.05, -61.8],
|
|
2988
|
+
"America/Araguaina": [-7.2, -48.2],
|
|
2989
|
+
"America/Argentina/Buenos_Aires": [-34.6, -58.45],
|
|
2990
|
+
"America/Argentina/Catamarca": [-28.47, -65.78],
|
|
2991
|
+
"America/Argentina/Cordoba": [-31.4, -64.18],
|
|
2992
|
+
"America/Argentina/Jujuy": [-24.18, -65.3],
|
|
2993
|
+
"America/Argentina/La_Rioja": [-29.43, -66.85],
|
|
2994
|
+
"America/Argentina/Mendoza": [-32.88, -68.82],
|
|
2995
|
+
"America/Argentina/Rio_Gallegos": [-51.63, -69.22],
|
|
2996
|
+
"America/Argentina/Salta": [-24.78, -65.42],
|
|
2997
|
+
"America/Argentina/San_Juan": [-31.53, -68.52],
|
|
2998
|
+
"America/Argentina/San_Luis": [-33.32, -66.35],
|
|
2999
|
+
"America/Argentina/Tucuman": [-26.82, -65.22],
|
|
3000
|
+
"America/Argentina/Ushuaia": [-54.8, -68.3],
|
|
3001
|
+
"America/Aruba": [12.5, -69.97],
|
|
3002
|
+
"America/Asuncion": [-25.27, -57.67],
|
|
3003
|
+
"America/Atikokan": [48.76, -91.62],
|
|
3004
|
+
"America/Bahia": [-12.98, -38.52],
|
|
3005
|
+
"America/Bahia_Banderas": [20.8, -105.25],
|
|
3006
|
+
"America/Barbados": [13.1, -59.62],
|
|
3007
|
+
"America/Belem": [-1.45, -48.48],
|
|
3008
|
+
"America/Belize": [17.5, -88.2],
|
|
3009
|
+
"America/Blanc-Sablon": [51.42, -57.12],
|
|
3010
|
+
"America/Boa_Vista": [2.82, -60.67],
|
|
3011
|
+
"America/Bogota": [4.6, -74.08],
|
|
3012
|
+
"America/Boise": [43.61, -116.2],
|
|
3013
|
+
"America/Cambridge_Bay": [69.11, -105.05],
|
|
3014
|
+
"America/Campo_Grande": [-20.45, -54.62],
|
|
3015
|
+
"America/Cancun": [21.08, -86.77],
|
|
3016
|
+
"America/Caracas": [10.5, -66.93],
|
|
3017
|
+
"America/Cayenne": [4.93, -52.33],
|
|
3018
|
+
"America/Cayman": [19.3, -81.38],
|
|
3019
|
+
"America/Chicago": [41.85, -87.65],
|
|
3020
|
+
"America/Chihuahua": [28.63, -106.08],
|
|
3021
|
+
"America/Ciudad_Juarez": [31.73, -106.48],
|
|
3022
|
+
"America/Costa_Rica": [9.93, -84.08],
|
|
3023
|
+
"America/Coyhaique": [-45.57, -72.07],
|
|
3024
|
+
"America/Creston": [49.1, -116.52],
|
|
3025
|
+
"America/Cuiaba": [-15.58, -56.08],
|
|
3026
|
+
"America/Curacao": [12.18, -69],
|
|
3027
|
+
"America/Danmarkshavn": [76.77, -18.67],
|
|
3028
|
+
"America/Dawson": [64.07, -139.42],
|
|
3029
|
+
"America/Dawson_Creek": [55.77, -120.23],
|
|
3030
|
+
"America/Denver": [39.74, -104.98],
|
|
3031
|
+
"America/Detroit": [42.33, -83.05],
|
|
3032
|
+
"America/Dominica": [15.3, -61.4],
|
|
3033
|
+
"America/Edmonton": [53.55, -113.47],
|
|
3034
|
+
"America/Eirunepe": [-6.67, -69.87],
|
|
3035
|
+
"America/El_Salvador": [13.7, -89.2],
|
|
3036
|
+
"America/Fort_Nelson": [58.8, -122.7],
|
|
3037
|
+
"America/Fortaleza": [-3.72, -38.5],
|
|
3038
|
+
"America/Glace_Bay": [46.2, -59.95],
|
|
3039
|
+
"America/Goose_Bay": [53.33, -60.42],
|
|
3040
|
+
"America/Grand_Turk": [21.47, -71.13],
|
|
3041
|
+
"America/Grenada": [12.05, -61.75],
|
|
3042
|
+
"America/Guadeloupe": [16.23, -61.53],
|
|
3043
|
+
"America/Guatemala": [14.63, -90.52],
|
|
3044
|
+
"America/Guayaquil": [-2.17, -79.83],
|
|
3045
|
+
"America/Guyana": [6.8, -58.17],
|
|
3046
|
+
"America/Halifax": [44.65, -63.6],
|
|
3047
|
+
"America/Havana": [23.13, -82.37],
|
|
3048
|
+
"America/Hermosillo": [29.07, -110.97],
|
|
3049
|
+
"America/Indiana/Indianapolis": [39.77, -86.16],
|
|
3050
|
+
"America/Indiana/Knox": [41.3, -86.62],
|
|
3051
|
+
"America/Indiana/Marengo": [38.38, -86.34],
|
|
3052
|
+
"America/Indiana/Petersburg": [38.49, -87.28],
|
|
3053
|
+
"America/Indiana/Tell_City": [37.95, -86.76],
|
|
3054
|
+
"America/Indiana/Vevay": [38.75, -85.07],
|
|
3055
|
+
"America/Indiana/Vincennes": [38.68, -87.53],
|
|
3056
|
+
"America/Indiana/Winamac": [41.05, -86.6],
|
|
3057
|
+
"America/Inuvik": [68.35, -133.72],
|
|
3058
|
+
"America/Iqaluit": [63.73, -68.47],
|
|
3059
|
+
"America/Jamaica": [17.97, -76.79],
|
|
3060
|
+
"America/Juneau": [58.3, -134.42],
|
|
3061
|
+
"America/Kentucky/Louisville": [38.25, -85.76],
|
|
3062
|
+
"America/Kentucky/Monticello": [36.83, -84.85],
|
|
3063
|
+
"America/Kralendijk": [12.15, -68.28],
|
|
3064
|
+
"America/La_Paz": [-16.5, -68.15],
|
|
3065
|
+
"America/Lima": [-12.05, -77.05],
|
|
3066
|
+
"America/Los_Angeles": [34.05, -118.24],
|
|
3067
|
+
"America/Lower_Princes": [18.05, -63.05],
|
|
3068
|
+
"America/Maceio": [-9.67, -35.72],
|
|
3069
|
+
"America/Managua": [12.15, -86.28],
|
|
3070
|
+
"America/Manaus": [-3.13, -60.02],
|
|
3071
|
+
"America/Marigot": [18.07, -63.08],
|
|
3072
|
+
"America/Martinique": [14.6, -61.08],
|
|
3073
|
+
"America/Matamoros": [25.83, -97.5],
|
|
3074
|
+
"America/Mazatlan": [23.22, -106.42],
|
|
3075
|
+
"America/Menominee": [45.11, -87.61],
|
|
3076
|
+
"America/Merida": [20.97, -89.62],
|
|
3077
|
+
"America/Metlakatla": [55.13, -131.58],
|
|
3078
|
+
"America/Mexico_City": [19.4, -99.15],
|
|
3079
|
+
"America/Miquelon": [47.05, -56.33],
|
|
3080
|
+
"America/Moncton": [46.1, -64.78],
|
|
3081
|
+
"America/Monterrey": [25.67, -100.32],
|
|
3082
|
+
"America/Montevideo": [-34.91, -56.21],
|
|
3083
|
+
"America/Montserrat": [16.72, -62.22],
|
|
3084
|
+
"America/Nassau": [25.08, -77.35],
|
|
3085
|
+
"America/New_York": [40.71, -74.01],
|
|
3086
|
+
"America/Nome": [64.5, -165.41],
|
|
3087
|
+
"America/Noronha": [-3.85, -32.42],
|
|
3088
|
+
"America/North_Dakota/Beulah": [47.26, -101.78],
|
|
3089
|
+
"America/North_Dakota/Center": [47.12, -101.3],
|
|
3090
|
+
"America/North_Dakota/New_Salem": [46.84, -101.41],
|
|
3091
|
+
"America/Nuuk": [64.18, -51.73],
|
|
3092
|
+
"America/Ojinaga": [29.57, -104.42],
|
|
3093
|
+
"America/Panama": [8.97, -79.53],
|
|
3094
|
+
"America/Paramaribo": [5.83, -55.17],
|
|
3095
|
+
"America/Phoenix": [33.45, -112.07],
|
|
3096
|
+
"America/Port-au-Prince": [18.53, -72.33],
|
|
3097
|
+
"America/Port_of_Spain": [10.65, -61.52],
|
|
3098
|
+
"America/Porto_Velho": [-8.77, -63.9],
|
|
3099
|
+
"America/Puerto_Rico": [18.47, -66.11],
|
|
3100
|
+
"America/Punta_Arenas": [-53.15, -70.92],
|
|
3101
|
+
"America/Rankin_Inlet": [62.82, -92.08],
|
|
3102
|
+
"America/Recife": [-8.05, -34.9],
|
|
3103
|
+
"America/Regina": [50.4, -104.65],
|
|
3104
|
+
"America/Resolute": [74.7, -94.83],
|
|
3105
|
+
"America/Rio_Branco": [-9.97, -67.8],
|
|
3106
|
+
"America/Santarem": [-2.43, -54.87],
|
|
3107
|
+
"America/Santiago": [-33.45, -70.67],
|
|
3108
|
+
"America/Santo_Domingo": [18.47, -69.9],
|
|
3109
|
+
"America/Sao_Paulo": [-23.53, -46.62],
|
|
3110
|
+
"America/Scoresbysund": [70.48, -21.97],
|
|
3111
|
+
"America/Sitka": [57.18, -135.3],
|
|
3112
|
+
"America/St_Barthelemy": [17.88, -62.85],
|
|
3113
|
+
"America/St_Johns": [47.57, -52.72],
|
|
3114
|
+
"America/St_Kitts": [17.3, -62.72],
|
|
3115
|
+
"America/St_Lucia": [14.02, -61],
|
|
3116
|
+
"America/St_Thomas": [18.35, -64.93],
|
|
3117
|
+
"America/St_Vincent": [13.15, -61.23],
|
|
3118
|
+
"America/Swift_Current": [50.28, -107.83],
|
|
3119
|
+
"America/Tegucigalpa": [14.1, -87.22],
|
|
3120
|
+
"America/Thule": [76.57, -68.78],
|
|
3121
|
+
"America/Tijuana": [32.53, -117.02],
|
|
3122
|
+
"America/Toronto": [43.65, -79.38],
|
|
3123
|
+
"America/Tortola": [18.45, -64.62],
|
|
3124
|
+
"America/Vancouver": [49.27, -123.12],
|
|
3125
|
+
"America/Whitehorse": [60.72, -135.05],
|
|
3126
|
+
"America/Winnipeg": [49.88, -97.15],
|
|
3127
|
+
"America/Yakutat": [59.55, -139.73],
|
|
3128
|
+
"Antarctica/Casey": [-66.28, 110.52],
|
|
3129
|
+
"Antarctica/Davis": [-68.58, 77.97],
|
|
3130
|
+
"Antarctica/DumontDUrville": [-66.67, 140.02],
|
|
3131
|
+
"Antarctica/Macquarie": [-54.5, 158.95],
|
|
3132
|
+
"Antarctica/Mawson": [-67.6, 62.88],
|
|
3133
|
+
"Antarctica/McMurdo": [-77.83, 166.6],
|
|
3134
|
+
"Antarctica/Palmer": [-64.8, -64.1],
|
|
3135
|
+
"Antarctica/Rothera": [-67.57, -68.13],
|
|
3136
|
+
"Antarctica/Syowa": [-69.01, 39.59],
|
|
3137
|
+
"Antarctica/Troll": [-72.01, 2.53],
|
|
3138
|
+
"Antarctica/Vostok": [-78.4, 106.9],
|
|
3139
|
+
"Arctic/Longyearbyen": [78, 16],
|
|
3140
|
+
"Asia/Aden": [12.75, 45.2],
|
|
3141
|
+
"Asia/Almaty": [43.25, 76.95],
|
|
3142
|
+
"Asia/Amman": [31.95, 35.93],
|
|
3143
|
+
"Asia/Anadyr": [64.75, 177.48],
|
|
3144
|
+
"Asia/Aqtau": [44.52, 50.27],
|
|
3145
|
+
"Asia/Aqtobe": [50.28, 57.17],
|
|
3146
|
+
"Asia/Ashgabat": [37.95, 58.38],
|
|
3147
|
+
"Asia/Atyrau": [47.12, 51.93],
|
|
3148
|
+
"Asia/Baghdad": [33.35, 44.42],
|
|
3149
|
+
"Asia/Bahrain": [26.38, 50.58],
|
|
3150
|
+
"Asia/Baku": [40.38, 49.85],
|
|
3151
|
+
"Asia/Bangkok": [13.75, 100.52],
|
|
3152
|
+
"Asia/Barnaul": [53.37, 83.75],
|
|
3153
|
+
"Asia/Beirut": [33.88, 35.5],
|
|
3154
|
+
"Asia/Bishkek": [42.9, 74.6],
|
|
3155
|
+
"Asia/Brunei": [4.93, 114.92],
|
|
3156
|
+
"Asia/Chita": [52.05, 113.47],
|
|
3157
|
+
"Asia/Colombo": [6.93, 79.85],
|
|
3158
|
+
"Asia/Damascus": [33.5, 36.3],
|
|
3159
|
+
"Asia/Dhaka": [23.72, 90.42],
|
|
3160
|
+
"Asia/Dili": [-8.55, 125.58],
|
|
3161
|
+
"Asia/Dubai": [25.3, 55.3],
|
|
3162
|
+
"Asia/Dushanbe": [38.58, 68.8],
|
|
3163
|
+
"Asia/Famagusta": [35.12, 33.95],
|
|
3164
|
+
"Asia/Gaza": [31.5, 34.47],
|
|
3165
|
+
"Asia/Hebron": [31.53, 35.09],
|
|
3166
|
+
"Asia/Ho_Chi_Minh": [10.75, 106.67],
|
|
3167
|
+
"Asia/Hong_Kong": [22.28, 114.15],
|
|
3168
|
+
"Asia/Hovd": [48.02, 91.65],
|
|
3169
|
+
"Asia/Irkutsk": [52.27, 104.33],
|
|
3170
|
+
"Asia/Jakarta": [-6.17, 106.8],
|
|
3171
|
+
"Asia/Jayapura": [-2.53, 140.7],
|
|
3172
|
+
"Asia/Jerusalem": [31.78, 35.22],
|
|
3173
|
+
"Asia/Kabul": [34.52, 69.2],
|
|
3174
|
+
"Asia/Kamchatka": [53.02, 158.65],
|
|
3175
|
+
"Asia/Karachi": [24.87, 67.05],
|
|
3176
|
+
"Asia/Kathmandu": [27.72, 85.32],
|
|
3177
|
+
"Asia/Khandyga": [62.66, 135.55],
|
|
3178
|
+
"Asia/Kolkata": [22.53, 88.37],
|
|
3179
|
+
"Asia/Krasnoyarsk": [56.02, 92.83],
|
|
3180
|
+
"Asia/Kuala_Lumpur": [3.17, 101.7],
|
|
3181
|
+
"Asia/Kuching": [1.55, 110.33],
|
|
3182
|
+
"Asia/Kuwait": [29.33, 47.98],
|
|
3183
|
+
"Asia/Macau": [22.2, 113.54],
|
|
3184
|
+
"Asia/Magadan": [59.57, 150.8],
|
|
3185
|
+
"Asia/Makassar": [-5.12, 119.4],
|
|
3186
|
+
"Asia/Manila": [14.59, 120.97],
|
|
3187
|
+
"Asia/Muscat": [23.6, 58.58],
|
|
3188
|
+
"Asia/Nicosia": [35.17, 33.37],
|
|
3189
|
+
"Asia/Novokuznetsk": [53.75, 87.12],
|
|
3190
|
+
"Asia/Novosibirsk": [55.03, 82.92],
|
|
3191
|
+
"Asia/Omsk": [55, 73.4],
|
|
3192
|
+
"Asia/Oral": [51.22, 51.35],
|
|
3193
|
+
"Asia/Phnom_Penh": [11.55, 104.92],
|
|
3194
|
+
"Asia/Pontianak": [-0.03, 109.33],
|
|
3195
|
+
"Asia/Pyongyang": [39.02, 125.75],
|
|
3196
|
+
"Asia/Qatar": [25.28, 51.53],
|
|
3197
|
+
"Asia/Qostanay": [53.2, 63.62],
|
|
3198
|
+
"Asia/Qyzylorda": [44.8, 65.47],
|
|
3199
|
+
"Asia/Riyadh": [24.63, 46.72],
|
|
3200
|
+
"Asia/Sakhalin": [46.97, 142.7],
|
|
3201
|
+
"Asia/Samarkand": [39.67, 66.8],
|
|
3202
|
+
"Asia/Seoul": [37.55, 126.97],
|
|
3203
|
+
"Asia/Shanghai": [31.23, 121.47],
|
|
3204
|
+
"Asia/Singapore": [1.28, 103.85],
|
|
3205
|
+
"Asia/Srednekolymsk": [67.47, 153.72],
|
|
3206
|
+
"Asia/Taipei": [25.05, 121.5],
|
|
3207
|
+
"Asia/Tashkent": [41.33, 69.3],
|
|
3208
|
+
"Asia/Tbilisi": [41.72, 44.82],
|
|
3209
|
+
"Asia/Tehran": [35.67, 51.43],
|
|
3210
|
+
"Asia/Thimphu": [27.47, 89.65],
|
|
3211
|
+
"Asia/Tokyo": [35.65, 139.74],
|
|
3212
|
+
"Asia/Tomsk": [56.5, 84.97],
|
|
3213
|
+
"Asia/Ulaanbaatar": [47.92, 106.88],
|
|
3214
|
+
"Asia/Urumqi": [43.8, 87.58],
|
|
3215
|
+
"Asia/Ust-Nera": [64.56, 143.23],
|
|
3216
|
+
"Asia/Vientiane": [17.97, 102.6],
|
|
3217
|
+
"Asia/Vladivostok": [43.17, 131.93],
|
|
3218
|
+
"Asia/Yakutsk": [62, 129.67],
|
|
3219
|
+
"Asia/Yangon": [16.78, 96.17],
|
|
3220
|
+
"Asia/Yekaterinburg": [56.85, 60.6],
|
|
3221
|
+
"Asia/Yerevan": [40.18, 44.5],
|
|
3222
|
+
"Atlantic/Azores": [37.73, -25.67],
|
|
3223
|
+
"Atlantic/Bermuda": [32.28, -64.77],
|
|
3224
|
+
"Atlantic/Canary": [28.1, -15.4],
|
|
3225
|
+
"Atlantic/Cape_Verde": [14.92, -23.52],
|
|
3226
|
+
"Atlantic/Faroe": [62.02, -6.77],
|
|
3227
|
+
"Atlantic/Madeira": [32.63, -16.9],
|
|
3228
|
+
"Atlantic/Reykjavik": [64.15, -21.85],
|
|
3229
|
+
"Atlantic/South_Georgia": [-54.27, -36.53],
|
|
3230
|
+
"Atlantic/St_Helena": [-15.92, -5.7],
|
|
3231
|
+
"Atlantic/Stanley": [-51.7, -57.85],
|
|
3232
|
+
"Australia/Adelaide": [-34.92, 138.58],
|
|
3233
|
+
"Australia/Brisbane": [-27.47, 153.03],
|
|
3234
|
+
"Australia/Broken_Hill": [-31.95, 141.45],
|
|
3235
|
+
"Australia/Darwin": [-12.47, 130.83],
|
|
3236
|
+
"Australia/Eucla": [-31.72, 128.87],
|
|
3237
|
+
"Australia/Hobart": [-42.88, 147.32],
|
|
3238
|
+
"Australia/Lindeman": [-20.27, 149],
|
|
3239
|
+
"Australia/Lord_Howe": [-31.55, 159.08],
|
|
3240
|
+
"Australia/Melbourne": [-37.82, 144.97],
|
|
3241
|
+
"Australia/Perth": [-31.95, 115.85],
|
|
3242
|
+
"Australia/Sydney": [-33.87, 151.22],
|
|
3243
|
+
"Europe/Amsterdam": [52.37, 4.9],
|
|
3244
|
+
"Europe/Andorra": [42.5, 1.52],
|
|
3245
|
+
"Europe/Astrakhan": [46.35, 48.05],
|
|
3246
|
+
"Europe/Athens": [37.97, 23.72],
|
|
3247
|
+
"Europe/Belgrade": [44.83, 20.5],
|
|
3248
|
+
"Europe/Berlin": [52.5, 13.37],
|
|
3249
|
+
"Europe/Bratislava": [48.15, 17.12],
|
|
3250
|
+
"Europe/Brussels": [50.83, 4.33],
|
|
3251
|
+
"Europe/Bucharest": [44.43, 26.1],
|
|
3252
|
+
"Europe/Budapest": [47.5, 19.08],
|
|
3253
|
+
"Europe/Busingen": [47.7, 8.68],
|
|
3254
|
+
"Europe/Chisinau": [47, 28.83],
|
|
3255
|
+
"Europe/Copenhagen": [55.67, 12.58],
|
|
3256
|
+
"Europe/Dublin": [53.33, -6.25],
|
|
3257
|
+
"Europe/Gibraltar": [36.13, -5.35],
|
|
3258
|
+
"Europe/Guernsey": [49.45, -2.54],
|
|
3259
|
+
"Europe/Helsinki": [60.17, 24.97],
|
|
3260
|
+
"Europe/Isle_of_Man": [54.15, -4.47],
|
|
3261
|
+
"Europe/Istanbul": [41.02, 28.97],
|
|
3262
|
+
"Europe/Jersey": [49.18, -2.11],
|
|
3263
|
+
"Europe/Kaliningrad": [54.72, 20.5],
|
|
3264
|
+
"Europe/Kirov": [58.6, 49.65],
|
|
3265
|
+
"Europe/Kyiv": [50.43, 30.52],
|
|
3266
|
+
"Europe/Lisbon": [38.72, -9.13],
|
|
3267
|
+
"Europe/Ljubljana": [46.05, 14.52],
|
|
3268
|
+
"Europe/London": [51.51, -0.13],
|
|
3269
|
+
"Europe/Luxembourg": [49.6, 6.15],
|
|
3270
|
+
"Europe/Madrid": [40.4, -3.68],
|
|
3271
|
+
"Europe/Malta": [35.9, 14.52],
|
|
3272
|
+
"Europe/Mariehamn": [60.1, 19.95],
|
|
3273
|
+
"Europe/Minsk": [53.9, 27.57],
|
|
3274
|
+
"Europe/Monaco": [43.7, 7.38],
|
|
3275
|
+
"Europe/Moscow": [55.76, 37.62],
|
|
3276
|
+
"Europe/Oslo": [59.92, 10.75],
|
|
3277
|
+
"Europe/Paris": [48.87, 2.33],
|
|
3278
|
+
"Europe/Podgorica": [42.43, 19.27],
|
|
3279
|
+
"Europe/Prague": [50.08, 14.43],
|
|
3280
|
+
"Europe/Riga": [56.95, 24.1],
|
|
3281
|
+
"Europe/Rome": [41.9, 12.48],
|
|
3282
|
+
"Europe/Samara": [53.2, 50.15],
|
|
3283
|
+
"Europe/San_Marino": [43.92, 12.47],
|
|
3284
|
+
"Europe/Sarajevo": [43.87, 18.42],
|
|
3285
|
+
"Europe/Saratov": [51.57, 46.03],
|
|
3286
|
+
"Europe/Simferopol": [44.95, 34.1],
|
|
3287
|
+
"Europe/Skopje": [41.98, 21.43],
|
|
3288
|
+
"Europe/Sofia": [42.68, 23.32],
|
|
3289
|
+
"Europe/Stockholm": [59.33, 18.05],
|
|
3290
|
+
"Europe/Tallinn": [59.42, 24.75],
|
|
3291
|
+
"Europe/Tirane": [41.33, 19.83],
|
|
3292
|
+
"Europe/Ulyanovsk": [54.33, 48.4],
|
|
3293
|
+
"Europe/Vaduz": [47.15, 9.52],
|
|
3294
|
+
"Europe/Vatican": [41.9, 12.45],
|
|
3295
|
+
"Europe/Vienna": [48.22, 16.33],
|
|
3296
|
+
"Europe/Vilnius": [54.68, 25.32],
|
|
3297
|
+
"Europe/Volgograd": [48.73, 44.42],
|
|
3298
|
+
"Europe/Warsaw": [52.25, 21],
|
|
3299
|
+
"Europe/Zagreb": [45.8, 15.97],
|
|
3300
|
+
"Europe/Zurich": [47.38, 8.53],
|
|
3301
|
+
"Indian/Antananarivo": [-18.92, 47.52],
|
|
3302
|
+
"Indian/Chagos": [-7.33, 72.42],
|
|
3303
|
+
"Indian/Christmas": [-10.42, 105.72],
|
|
3304
|
+
"Indian/Cocos": [-12.17, 96.92],
|
|
3305
|
+
"Indian/Comoro": [-11.68, 43.27],
|
|
3306
|
+
"Indian/Kerguelen": [-49.35, 70.22],
|
|
3307
|
+
"Indian/Mahe": [-4.67, 55.47],
|
|
3308
|
+
"Indian/Maldives": [4.17, 73.5],
|
|
3309
|
+
"Indian/Mauritius": [-20.17, 57.5],
|
|
3310
|
+
"Indian/Mayotte": [-12.78, 45.23],
|
|
3311
|
+
"Indian/Reunion": [-20.87, 55.47],
|
|
3312
|
+
"Pacific/Apia": [-13.83, -171.73],
|
|
3313
|
+
"Pacific/Auckland": [-36.87, 174.77],
|
|
3314
|
+
"Pacific/Bougainville": [-6.22, 155.57],
|
|
3315
|
+
"Pacific/Chatham": [-43.95, -176.55],
|
|
3316
|
+
"Pacific/Chuuk": [7.42, 151.78],
|
|
3317
|
+
"Pacific/Easter": [-27.15, -109.43],
|
|
3318
|
+
"Pacific/Efate": [-17.67, 168.42],
|
|
3319
|
+
"Pacific/Fakaofo": [-9.37, -171.23],
|
|
3320
|
+
"Pacific/Fiji": [-18.13, 178.42],
|
|
3321
|
+
"Pacific/Funafuti": [-8.52, 179.22],
|
|
3322
|
+
"Pacific/Galapagos": [-0.9, -89.6],
|
|
3323
|
+
"Pacific/Gambier": [-23.13, -134.95],
|
|
3324
|
+
"Pacific/Guadalcanal": [-9.53, 160.2],
|
|
3325
|
+
"Pacific/Guam": [13.47, 144.75],
|
|
3326
|
+
"Pacific/Honolulu": [21.31, -157.86],
|
|
3327
|
+
"Pacific/Kanton": [-2.78, -171.72],
|
|
3328
|
+
"Pacific/Kiritimati": [1.87, -157.33],
|
|
3329
|
+
"Pacific/Kosrae": [5.32, 162.98],
|
|
3330
|
+
"Pacific/Kwajalein": [9.08, 167.33],
|
|
3331
|
+
"Pacific/Majuro": [7.15, 171.2],
|
|
3332
|
+
"Pacific/Marquesas": [-9, -139.5],
|
|
3333
|
+
"Pacific/Midway": [28.22, -177.37],
|
|
3334
|
+
"Pacific/Nauru": [-0.52, 166.92],
|
|
3335
|
+
"Pacific/Niue": [-19.02, -169.92],
|
|
3336
|
+
"Pacific/Norfolk": [-29.05, 167.97],
|
|
3337
|
+
"Pacific/Noumea": [-22.27, 166.45],
|
|
3338
|
+
"Pacific/Pago_Pago": [-14.27, -170.7],
|
|
3339
|
+
"Pacific/Palau": [7.33, 134.48],
|
|
3340
|
+
"Pacific/Pitcairn": [-25.07, -130.08],
|
|
3341
|
+
"Pacific/Pohnpei": [6.97, 158.22],
|
|
3342
|
+
"Pacific/Port_Moresby": [-9.5, 147.17],
|
|
3343
|
+
"Pacific/Rarotonga": [-21.23, -159.77],
|
|
3344
|
+
"Pacific/Saipan": [15.2, 145.75],
|
|
3345
|
+
"Pacific/Tahiti": [-17.53, -149.57],
|
|
3346
|
+
"Pacific/Tarawa": [1.42, 173],
|
|
3347
|
+
"Pacific/Tongatapu": [-21.13, -175.2],
|
|
3348
|
+
"Pacific/Wake": [19.28, 166.62],
|
|
3349
|
+
"Pacific/Wallis": [-13.3, -176.17]
|
|
3350
|
+
};
|
|
3351
|
+
var ALIASES = {
|
|
3352
|
+
"Africa/Asmera": "Africa/Asmara",
|
|
3353
|
+
"America/Buenos_Aires": "America/Argentina/Buenos_Aires",
|
|
3354
|
+
"America/Catamarca": "America/Argentina/Catamarca",
|
|
3355
|
+
"America/Coral_Harbour": "America/Atikokan",
|
|
3356
|
+
"America/Cordoba": "America/Argentina/Cordoba",
|
|
3357
|
+
"America/Godthab": "America/Nuuk",
|
|
3358
|
+
"America/Indianapolis": "America/Indiana/Indianapolis",
|
|
3359
|
+
"America/Jujuy": "America/Argentina/Jujuy",
|
|
3360
|
+
"America/Louisville": "America/Kentucky/Louisville",
|
|
3361
|
+
"America/Mendoza": "America/Argentina/Mendoza",
|
|
3362
|
+
"Asia/Calcutta": "Asia/Kolkata",
|
|
3363
|
+
"Asia/Katmandu": "Asia/Kathmandu",
|
|
3364
|
+
"Asia/Rangoon": "Asia/Yangon",
|
|
3365
|
+
"Asia/Saigon": "Asia/Ho_Chi_Minh",
|
|
3366
|
+
"Atlantic/Faeroe": "Atlantic/Faroe",
|
|
3367
|
+
"Europe/Kiev": "Europe/Kyiv",
|
|
3368
|
+
"Pacific/Enderbury": "Pacific/Kanton",
|
|
3369
|
+
"Pacific/Ponape": "Pacific/Pohnpei",
|
|
3370
|
+
"Pacific/Truk": "Pacific/Chuuk"
|
|
3371
|
+
};
|
|
3372
|
+
function placeForZone(zone) {
|
|
3373
|
+
const coords = COORDS[zone] ?? COORDS[ALIASES[zone]];
|
|
3374
|
+
if (!coords) return null;
|
|
3375
|
+
return { zone, lat: coords[0], lon: coords[1], label: zoneLabel(zone) };
|
|
3376
|
+
}
|
|
3377
|
+
var PLACE_OPTIONS = Object.keys(COORDS).map((zone) => ({ value: zone, label: zoneLabel(zone) })).sort((a, b) => a.label.localeCompare(b.label));
|
|
3167
3378
|
|
|
3168
3379
|
// src/lib/store.ts
|
|
3169
3380
|
var DEFAULT_SETTINGS = {
|
|
3381
|
+
// Midnight is not "a preset that happens to be first": it is the app every
|
|
3382
|
+
// existing user has, so it is the default and the other two are opt-in.
|
|
3383
|
+
theme: DEFAULT_THEME,
|
|
3170
3384
|
// The 0–23 face. It is the one that matches the instrument: a revolution is
|
|
3171
3385
|
// a day here, not half of one, so an hour has exactly one place on it and
|
|
3172
3386
|
// `13` is where `13` is. AM/PM is a preference, and it is one click away.
|
|
@@ -3283,12 +3497,39 @@ var DEFAULT_SETTINGS = {
|
|
|
3283
3497
|
// `DEFAULT_WAKING`'s own pair: this setting exists to replace the constant,
|
|
3284
3498
|
// not to disagree with it on day one.
|
|
3285
3499
|
wakingFrom: DEFAULT_WAKING.from,
|
|
3286
|
-
wakingTo: DEFAULT_WAKING.to
|
|
3500
|
+
wakingTo: DEFAULT_WAKING.to,
|
|
3501
|
+
// Opt-in: writes to the calendar nobody asked for are the one thing an
|
|
3502
|
+
// instrument must never surprise anybody with.
|
|
3503
|
+
autoSchedule: false
|
|
3287
3504
|
};
|
|
3288
3505
|
var DEFAULT_CALENDARS = [
|
|
3289
|
-
{
|
|
3290
|
-
|
|
3291
|
-
|
|
3506
|
+
{
|
|
3507
|
+
id: "personal",
|
|
3508
|
+
name: "Personal",
|
|
3509
|
+
color: PALETTE[0],
|
|
3510
|
+
icon: "house",
|
|
3511
|
+
visible: true,
|
|
3512
|
+
remindLead: REMIND_INHERIT,
|
|
3513
|
+
updatedAt: 0
|
|
3514
|
+
},
|
|
3515
|
+
{
|
|
3516
|
+
id: "work",
|
|
3517
|
+
name: "Work",
|
|
3518
|
+
color: PALETTE[3],
|
|
3519
|
+
icon: "briefcase",
|
|
3520
|
+
visible: true,
|
|
3521
|
+
remindLead: REMIND_INHERIT,
|
|
3522
|
+
updatedAt: 0
|
|
3523
|
+
},
|
|
3524
|
+
{
|
|
3525
|
+
id: "family",
|
|
3526
|
+
name: "Family",
|
|
3527
|
+
color: PALETTE[5],
|
|
3528
|
+
icon: "users",
|
|
3529
|
+
visible: true,
|
|
3530
|
+
remindLead: REMIND_INHERIT,
|
|
3531
|
+
updatedAt: 0
|
|
3532
|
+
}
|
|
3292
3533
|
];
|
|
3293
3534
|
function newId() {
|
|
3294
3535
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -3318,8 +3559,6 @@ function draftEvent(day, startMin, endMin, calendarId, repeat = "never") {
|
|
|
3318
3559
|
seriesKey: "",
|
|
3319
3560
|
color: "",
|
|
3320
3561
|
icon: "",
|
|
3321
|
-
task: false,
|
|
3322
|
-
doneKeys: [],
|
|
3323
3562
|
// Busy: a gesture that named a time meant that time to be spoken for.
|
|
3324
3563
|
free: false,
|
|
3325
3564
|
// Inheriting, which is the only answer a gesture gives: the drag named a
|
|
@@ -3331,10 +3570,26 @@ function draftEvent(day, startMin, endMin, calendarId, repeat = "never") {
|
|
|
3331
3570
|
// somebody else wrote.
|
|
3332
3571
|
organizer: null,
|
|
3333
3572
|
attendees: [],
|
|
3334
|
-
attendeeCount: 0
|
|
3573
|
+
attendeeCount: 0,
|
|
3574
|
+
// A draft has never been saved, so it has never been changed. `create` and
|
|
3575
|
+
// `update` are where a record gets its stamp; a `0` reaching storage would
|
|
3576
|
+
// be an event that predates stamping, which is the conservative loser in
|
|
3577
|
+
// every merge rather than a lie.
|
|
3578
|
+
updatedAt: 0
|
|
3335
3579
|
};
|
|
3336
3580
|
}
|
|
3337
3581
|
|
|
3582
|
+
// src/lib/ics.ts
|
|
3583
|
+
var encoder = new TextEncoder();
|
|
3584
|
+
function fnv1a(source) {
|
|
3585
|
+
let h = 2166136261;
|
|
3586
|
+
for (let i = 0; i < source.length; i++) {
|
|
3587
|
+
h ^= source.charCodeAt(i);
|
|
3588
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
3589
|
+
}
|
|
3590
|
+
return h.toString(36);
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3338
3593
|
// src/lib/backup.ts
|
|
3339
3594
|
function readSettings(value) {
|
|
3340
3595
|
if (!isRecord(value)) return DEFAULT_SETTINGS;
|
|
@@ -3344,6 +3599,10 @@ function readSettings(value) {
|
|
|
3344
3599
|
// there are exactly two spellings and both are written out by every
|
|
3345
3600
|
// version that has ever had the setting.
|
|
3346
3601
|
clock: value.clock === "12h" || value.clock === "24h" ? value.clock : DEFAULT_SETTINGS.clock,
|
|
3602
|
+
// A preset this build does not carry resets rather than half-applies:
|
|
3603
|
+
// `sane()` would pin it anyway, but a restored calendar should not spend
|
|
3604
|
+
// one commit looking broken first.
|
|
3605
|
+
theme: isThemeName(value.theme) ? value.theme : DEFAULT_SETTINGS.theme,
|
|
3347
3606
|
secondZone: typeof value.secondZone === "string" ? value.secondZone : "",
|
|
3348
3607
|
// The zone the browser that wrote this backup was in. Carried rather than
|
|
3349
3608
|
// dropped, and deliberately not replaced with the reader's own: a Berlin
|
|
@@ -3437,13 +3696,12 @@ function readSettings(value) {
|
|
|
3437
3696
|
// waking window to the defaults, same as every other bounded field here.
|
|
3438
3697
|
weekStart: value.weekStart === 0 || value.weekStart === 1 || value.weekStart === 6 ? value.weekStart : DEFAULT_SETTINGS.weekStart,
|
|
3439
3698
|
wakingFrom: typeof value.wakingFrom === "number" ? value.wakingFrom : DEFAULT_SETTINGS.wakingFrom,
|
|
3440
|
-
wakingTo: typeof value.wakingTo === "number" ? value.wakingTo : DEFAULT_SETTINGS.wakingTo
|
|
3699
|
+
wakingTo: typeof value.wakingTo === "number" ? value.wakingTo : DEFAULT_SETTINGS.wakingTo,
|
|
3700
|
+
// Off unless the backup says so — a restore must not start writing to
|
|
3701
|
+
// the calendar because the field was absent.
|
|
3702
|
+
autoSchedule: value.autoSchedule === true
|
|
3441
3703
|
};
|
|
3442
3704
|
}
|
|
3443
|
-
var INSTANT_KEY = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/;
|
|
3444
|
-
function readDoneKeys(value) {
|
|
3445
|
-
return Array.isArray(value) ? [...new Set(value.filter((k) => typeof k === "string" && INSTANT_KEY.test(k)))] : [];
|
|
3446
|
-
}
|
|
3447
3705
|
function derivedId(value, start, end) {
|
|
3448
3706
|
const text = (key) => typeof value[key] === "string" ? value[key] : "";
|
|
3449
3707
|
return `${start}-${fnv1a([start, end, text("title"), text("location"), text("seriesKey")].join("|"))}`;
|
|
@@ -3453,8 +3711,7 @@ function readEvent(value) {
|
|
|
3453
3711
|
const { id, title, calendarId, start, end } = value;
|
|
3454
3712
|
if (typeof start !== "number" || typeof end !== "number") return null;
|
|
3455
3713
|
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
3456
|
-
|
|
3457
|
-
if (task ? end < start : end <= start) return null;
|
|
3714
|
+
if (end <= start) return null;
|
|
3458
3715
|
return {
|
|
3459
3716
|
// Derived from the record's own fields, never minted: `Math.random()` gave
|
|
3460
3717
|
// a different id every read, so the same backup imported twice was two
|
|
@@ -3484,11 +3741,6 @@ function readEvent(value) {
|
|
|
3484
3741
|
// it has to tell apart. This used to slice at one shared cap, which turned
|
|
3485
3742
|
// ten of the 247 shipped names into strings matching no path.
|
|
3486
3743
|
icon: readMark(value.icon),
|
|
3487
|
-
task,
|
|
3488
|
-
// The same instant-key check EXDATE gets, and the same reason: a key this
|
|
3489
|
-
// does not spell matches no occurrence, so it is a row of dead weight that
|
|
3490
|
-
// survives every future export.
|
|
3491
|
-
doneKeys: readDoneKeys(value.doneKeys),
|
|
3492
3744
|
// Anything other than a literal `true` is busy, which is what an event
|
|
3493
3745
|
// exported before this field existed was. A truthy string in a
|
|
3494
3746
|
// hand-edited blob must not quietly take a meeting off the day's total.
|
|
@@ -3506,7 +3758,12 @@ function readEvent(value) {
|
|
|
3506
3758
|
// did — clamped up to the list it labels, since a blob understating it
|
|
3507
3759
|
// would print a negative remainder.
|
|
3508
3760
|
organizer: readPerson(value.organizer),
|
|
3509
|
-
...readGuests(value.attendees, value.attendeeCount)
|
|
3761
|
+
...readGuests(value.attendees, value.attendeeCount),
|
|
3762
|
+
// What a legal stamp is lives in `events.ts`, beside the graves it is
|
|
3763
|
+
// compared against. `0` for anything unreadable and for every backup taken
|
|
3764
|
+
// before stamping existed, which is what tells a merge that a document is
|
|
3765
|
+
// not one it can merge.
|
|
3766
|
+
updatedAt: readStamp(value.updatedAt)
|
|
3510
3767
|
};
|
|
3511
3768
|
}
|
|
3512
3769
|
function readCalendar(value) {
|
|
@@ -3524,14 +3781,44 @@ function readCalendar(value) {
|
|
|
3524
3781
|
// again should not lose the mark — but an unbounded string is a paste, and
|
|
3525
3782
|
// an over-long *name* is now dropped rather than cut into junk.
|
|
3526
3783
|
icon: readMark(icon),
|
|
3784
|
+
// Same validator as an event's lead, same reason: the middle level of the
|
|
3785
|
+
// inherit chain arrives from the same untrusted JSON the top one does, and
|
|
3786
|
+
// a second opinion about what a legal lead is would be `readRecurrence`'s
|
|
3787
|
+
// documented mistake. Absent reads as inheriting, which is what every
|
|
3788
|
+
// calendar written before this field existed already did.
|
|
3789
|
+
remindLead: readRemindLead(value.remindLead),
|
|
3527
3790
|
visible: value.visible !== false,
|
|
3791
|
+
updatedAt: readStamp(value.updatedAt),
|
|
3528
3792
|
...feedUrl ? { feed: { url: feedUrl, fetchedAt: 0, error: "" } } : {}
|
|
3529
3793
|
};
|
|
3530
3794
|
}
|
|
3795
|
+
function readTask(value) {
|
|
3796
|
+
if (!isRecord(value)) return null;
|
|
3797
|
+
const { id, title, due } = value;
|
|
3798
|
+
if (typeof id !== "string" || !id) return null;
|
|
3799
|
+
if (typeof due !== "number" || !Number.isFinite(due)) return null;
|
|
3800
|
+
return {
|
|
3801
|
+
id,
|
|
3802
|
+
title: typeof title === "string" ? title : "Untitled",
|
|
3803
|
+
notes: typeof value.notes === "string" ? value.notes : "",
|
|
3804
|
+
calendarId: typeof value.calendarId === "string" ? value.calendarId : "",
|
|
3805
|
+
// The mark and the colour take an event's own validators and defaults.
|
|
3806
|
+
icon: readMark(value.icon),
|
|
3807
|
+
color: typeof value.color === "string" ? value.color : "",
|
|
3808
|
+
due,
|
|
3809
|
+
dueAllDay: value.dueAllDay === true,
|
|
3810
|
+
estimate: readEstimate(value.estimate),
|
|
3811
|
+
priority: readPriority(value.priority),
|
|
3812
|
+
plans: readPlans(value.plans),
|
|
3813
|
+
done: value.done === true,
|
|
3814
|
+
updatedAt: readStamp(value.updatedAt)
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3531
3817
|
function readBackup(parsed) {
|
|
3532
3818
|
if (!isRecord(parsed) || parsed.app !== "circal") return { ok: false, reason: "not-circal" };
|
|
3533
3819
|
const events = (Array.isArray(parsed.events) ? parsed.events : []).map(readEvent).filter((e) => e !== null);
|
|
3534
3820
|
const calendars = (Array.isArray(parsed.calendars) ? parsed.calendars : []).map(readCalendar).filter((c) => c !== null);
|
|
3821
|
+
const tasks = (Array.isArray(parsed.tasks) ? parsed.tasks : []).map(readTask).filter((t) => t !== null);
|
|
3535
3822
|
if (calendars.length === 0) return { ok: false, reason: "no-calendars" };
|
|
3536
3823
|
const known = new Set(calendars.map((c) => c.id));
|
|
3537
3824
|
const fallback = calendars[0].id;
|
|
@@ -3543,12 +3830,19 @@ function readBackup(parsed) {
|
|
|
3543
3830
|
return e.seriesId && !seriesIds.has(e.seriesId) ? { ...parented, seriesId: "", seriesKey: "" } : parented;
|
|
3544
3831
|
}),
|
|
3545
3832
|
calendars,
|
|
3546
|
-
|
|
3833
|
+
// A task's calendar is parked the same way an event's is, same reason.
|
|
3834
|
+
tasks: tasks.map((t) => known.has(t.calendarId) ? t : { ...t, calendarId: fallback }),
|
|
3835
|
+
settings: readSettings(parsed.settings),
|
|
3836
|
+
// Not through `readSettings`: this is bookkeeping about the object rather
|
|
3837
|
+
// than a field of it, and a reader that clamped it into `Settings` would
|
|
3838
|
+
// have to teach every one of the thirty-eight about a stamp.
|
|
3839
|
+
settingsAt: readStamp(parsed.settingsAt),
|
|
3840
|
+
graves: readGraves(parsed.graves)
|
|
3547
3841
|
};
|
|
3548
3842
|
}
|
|
3549
3843
|
|
|
3550
3844
|
// src/lib/mirror.ts
|
|
3551
|
-
var MIRROR_VERSION =
|
|
3845
|
+
var MIRROR_VERSION = 3;
|
|
3552
3846
|
function settled(calendars) {
|
|
3553
3847
|
return calendars.map(
|
|
3554
3848
|
(c) => c.feed ? { ...c, feed: { ...c.feed, fetchedAt: 0, error: "" } } : c
|
|
@@ -3562,8 +3856,11 @@ function serialiseMirror(s, rev, zone = localZone()) {
|
|
|
3562
3856
|
zone,
|
|
3563
3857
|
rev,
|
|
3564
3858
|
events: s.events,
|
|
3859
|
+
tasks: s.tasks,
|
|
3565
3860
|
calendars: settled(s.calendars),
|
|
3566
|
-
settings: s.settings
|
|
3861
|
+
settings: s.settings,
|
|
3862
|
+
settingsAt: s.settingsAt,
|
|
3863
|
+
graves: s.graves
|
|
3567
3864
|
};
|
|
3568
3865
|
return `${JSON.stringify(file, null, 2)}
|
|
3569
3866
|
`;
|
|
@@ -3580,8 +3877,14 @@ function readMirrorFile(text) {
|
|
|
3580
3877
|
const envelope = parsed;
|
|
3581
3878
|
return {
|
|
3582
3879
|
events: read.events,
|
|
3880
|
+
tasks: read.tasks,
|
|
3583
3881
|
calendars: read.calendars,
|
|
3584
3882
|
settings: read.settings,
|
|
3883
|
+
// Both through `readBackup`, which is the boundary that already assumes
|
|
3884
|
+
// hostile input. A file that carries neither reads as a document that
|
|
3885
|
+
// predates stamping, and `mirrorMerges` declines to merge it.
|
|
3886
|
+
settingsAt: read.settingsAt,
|
|
3887
|
+
graves: read.graves,
|
|
3585
3888
|
rev: typeof envelope.rev === "number" && Number.isFinite(envelope.rev) ? envelope.rev : 0,
|
|
3586
3889
|
// A plain `.json` backup has no zone. Absent means "written by something
|
|
3587
3890
|
// that did not say", which the reader treats as agreeing with it — the
|
|
@@ -3594,30 +3897,128 @@ function readMirrorFile(text) {
|
|
|
3594
3897
|
};
|
|
3595
3898
|
}
|
|
3596
3899
|
|
|
3900
|
+
// src/lib/vault.ts
|
|
3901
|
+
var VAULT_VERSION = 1;
|
|
3902
|
+
var PBKDF2_ITERATIONS = 6e5;
|
|
3903
|
+
var VERSION_BYTES = 1;
|
|
3904
|
+
var SALT_BYTES = 16;
|
|
3905
|
+
var IV_BYTES = 12;
|
|
3906
|
+
var TAG_BYTES = 16;
|
|
3907
|
+
var HEADER_BYTES = VERSION_BYTES + SALT_BYTES + IV_BYTES;
|
|
3908
|
+
var MIN_BYTES = HEADER_BYTES + TAG_BYTES;
|
|
3909
|
+
var utf8 = new TextEncoder();
|
|
3910
|
+
var utf8Reader = new TextDecoder();
|
|
3911
|
+
var WrongPassphrase = class extends Error {
|
|
3912
|
+
};
|
|
3913
|
+
var UnreadablePayload = class extends Error {
|
|
3914
|
+
};
|
|
3915
|
+
function parse(payload) {
|
|
3916
|
+
const bytes = base64UrlToBytes(payload);
|
|
3917
|
+
if (!bytes || bytes.length < MIN_BYTES) return null;
|
|
3918
|
+
if (bytes[0] < 1 || bytes[0] > VAULT_VERSION) return null;
|
|
3919
|
+
return {
|
|
3920
|
+
salt: bytes.subarray(VERSION_BYTES, VERSION_BYTES + SALT_BYTES),
|
|
3921
|
+
iv: bytes.subarray(VERSION_BYTES + SALT_BYTES, HEADER_BYTES),
|
|
3922
|
+
body: bytes.subarray(HEADER_BYTES)
|
|
3923
|
+
};
|
|
3924
|
+
}
|
|
3925
|
+
function saltOf(payload) {
|
|
3926
|
+
const envelope = parse(payload);
|
|
3927
|
+
return envelope ? new Uint8Array(envelope.salt) : null;
|
|
3928
|
+
}
|
|
3929
|
+
async function deriveBits(passphrase, salt) {
|
|
3930
|
+
const material = await crypto.subtle.importKey("raw", utf8.encode(passphrase), "PBKDF2", false, [
|
|
3931
|
+
"deriveBits"
|
|
3932
|
+
]);
|
|
3933
|
+
const bits = await crypto.subtle.deriveBits(
|
|
3934
|
+
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
|
|
3935
|
+
material,
|
|
3936
|
+
256
|
|
3937
|
+
);
|
|
3938
|
+
return new Uint8Array(bits);
|
|
3939
|
+
}
|
|
3940
|
+
async function restoreKeyed(raw, salt) {
|
|
3941
|
+
const key = await crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, [
|
|
3942
|
+
"encrypt",
|
|
3943
|
+
"decrypt"
|
|
3944
|
+
]);
|
|
3945
|
+
return { key, salt };
|
|
3946
|
+
}
|
|
3947
|
+
function vaultOf({ key, salt }) {
|
|
3948
|
+
return {
|
|
3949
|
+
async seal(plaintext) {
|
|
3950
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
3951
|
+
const cipher = new Uint8Array(
|
|
3952
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, utf8.encode(plaintext))
|
|
3953
|
+
);
|
|
3954
|
+
const out = new Uint8Array(HEADER_BYTES + cipher.length);
|
|
3955
|
+
out[0] = VAULT_VERSION;
|
|
3956
|
+
out.set(salt, VERSION_BYTES);
|
|
3957
|
+
out.set(iv, VERSION_BYTES + SALT_BYTES);
|
|
3958
|
+
out.set(cipher, HEADER_BYTES);
|
|
3959
|
+
return bytesToBase64Url(out);
|
|
3960
|
+
},
|
|
3961
|
+
async open(payload) {
|
|
3962
|
+
const envelope = parse(payload);
|
|
3963
|
+
if (!envelope) throw new UnreadablePayload("not a vault payload this build can read");
|
|
3964
|
+
let plain;
|
|
3965
|
+
try {
|
|
3966
|
+
plain = await crypto.subtle.decrypt(
|
|
3967
|
+
{ name: "AES-GCM", iv: envelope.iv },
|
|
3968
|
+
key,
|
|
3969
|
+
envelope.body
|
|
3970
|
+
);
|
|
3971
|
+
} catch {
|
|
3972
|
+
throw new WrongPassphrase("passphrase does not open this payload");
|
|
3973
|
+
}
|
|
3974
|
+
return utf8Reader.decode(plain);
|
|
3975
|
+
}
|
|
3976
|
+
};
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
// src/lib/cloudLink.ts
|
|
3980
|
+
function readCloudRow(value) {
|
|
3981
|
+
if (typeof value !== "object" || value === null) return null;
|
|
3982
|
+
const row = value;
|
|
3983
|
+
if (typeof row.payload !== "string" || row.payload === "") return null;
|
|
3984
|
+
return {
|
|
3985
|
+
payload: row.payload,
|
|
3986
|
+
rev: typeof row.rev === "number" && Number.isFinite(row.rev) ? row.rev : 0,
|
|
3987
|
+
zone: typeof row.zone === "string" ? row.zone : "",
|
|
3988
|
+
version: typeof row.version === "number" && Number.isFinite(row.version) ? row.version : MIRROR_VERSION
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
var LOCKED = "That passphrase does not open the cloud copy. Nothing was written \u2014 this device's calendar is untouched.";
|
|
3992
|
+
var writing = Promise.resolve();
|
|
3993
|
+
|
|
3597
3994
|
// mcp/file.ts
|
|
3995
|
+
import { randomBytes } from "node:crypto";
|
|
3996
|
+
import { open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
3997
|
+
import { dirname, join } from "node:path";
|
|
3998
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
3598
3999
|
var RefusalError = class extends Error {
|
|
3599
4000
|
};
|
|
3600
4001
|
function resolveFilePath(argv2, env) {
|
|
3601
4002
|
const flagIndex = argv2.indexOf("--file");
|
|
3602
|
-
const
|
|
3603
|
-
const
|
|
3604
|
-
if (!
|
|
4003
|
+
const flagValue2 = flagIndex >= 0 ? argv2[flagIndex + 1] : void 0;
|
|
4004
|
+
const path = flagValue2 || env.CIRCAL_FILE;
|
|
4005
|
+
if (!path) {
|
|
3605
4006
|
throw new Error(
|
|
3606
4007
|
"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
4008
|
);
|
|
3608
4009
|
}
|
|
3609
|
-
return
|
|
4010
|
+
return path;
|
|
3610
4011
|
}
|
|
3611
|
-
async function readMirror(
|
|
3612
|
-
const text = await readFile(
|
|
4012
|
+
async function readMirror(path) {
|
|
4013
|
+
const text = await readFile(path, "utf8");
|
|
3613
4014
|
const contents = readMirrorFile(text);
|
|
3614
4015
|
if (!contents) {
|
|
3615
|
-
throw new Error(`${
|
|
4016
|
+
throw new Error(`${path} is not a circal mirror file (or a circal .json backup).`);
|
|
3616
4017
|
}
|
|
3617
4018
|
return contents;
|
|
3618
4019
|
}
|
|
3619
|
-
async function atomicWrite(
|
|
3620
|
-
const dir = dirname(
|
|
4020
|
+
async function atomicWrite(path, body) {
|
|
4021
|
+
const dir = dirname(path);
|
|
3621
4022
|
const tmp = join(dir, `.circal-mcp-${randomBytes(6).toString("hex")}.tmp`);
|
|
3622
4023
|
try {
|
|
3623
4024
|
const file = await open(tmp, "w");
|
|
@@ -3627,7 +4028,7 @@ async function atomicWrite(path2, body) {
|
|
|
3627
4028
|
} finally {
|
|
3628
4029
|
await file.close();
|
|
3629
4030
|
}
|
|
3630
|
-
await rename(tmp,
|
|
4031
|
+
await rename(tmp, path);
|
|
3631
4032
|
} catch (error) {
|
|
3632
4033
|
await rm(tmp, { force: true });
|
|
3633
4034
|
throw error;
|
|
@@ -3661,15 +4062,22 @@ function assertVersionAllowsMutation(contents) {
|
|
|
3661
4062
|
`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
4063
|
);
|
|
3663
4064
|
}
|
|
3664
|
-
async function loadForRead(
|
|
3665
|
-
const contents = await readMirror(
|
|
4065
|
+
async function loadForRead(ctx) {
|
|
4066
|
+
const contents = await readMirror(ctx.path);
|
|
3666
4067
|
return { contents, warning: zoneWarning(contents) };
|
|
3667
4068
|
}
|
|
4069
|
+
function fileStore(ctx) {
|
|
4070
|
+
return {
|
|
4071
|
+
transport: `file ${ctx.path}`,
|
|
4072
|
+
load: () => loadForRead(ctx),
|
|
4073
|
+
write: (mutate) => casWrite(ctx, mutate)
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
3668
4076
|
var LOCK_STALE_MS = 1e4;
|
|
3669
4077
|
var LOCK_WAIT_MS = 2e3;
|
|
3670
4078
|
var LOCK_POLL_MS = 100;
|
|
3671
|
-
async function acquireLock(
|
|
3672
|
-
const lock = `${
|
|
4079
|
+
async function acquireLock(path) {
|
|
4080
|
+
const lock = `${path}.lock`;
|
|
3673
4081
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
3674
4082
|
for (; ; ) {
|
|
3675
4083
|
try {
|
|
@@ -3700,15 +4108,15 @@ async function acquireLock(path2) {
|
|
|
3700
4108
|
await sleep(LOCK_POLL_MS);
|
|
3701
4109
|
}
|
|
3702
4110
|
}
|
|
3703
|
-
async function casWriteOnce(
|
|
3704
|
-
const lock = await acquireLock(
|
|
4111
|
+
async function casWriteOnce(ctx, mutate) {
|
|
4112
|
+
const lock = await acquireLock(ctx.path);
|
|
3705
4113
|
try {
|
|
3706
4114
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3707
|
-
const before = await readMirror(
|
|
4115
|
+
const before = await readMirror(ctx.path);
|
|
3708
4116
|
assertVersionAllowsMutation(before);
|
|
3709
|
-
assertZoneAllowsMutation(before,
|
|
4117
|
+
assertZoneAllowsMutation(before, ctx.env);
|
|
3710
4118
|
const { result, summary } = mutate(before);
|
|
3711
|
-
const after = await readMirror(
|
|
4119
|
+
const after = await readMirror(ctx.path);
|
|
3712
4120
|
if (after.rev !== before.rev) {
|
|
3713
4121
|
if (attempt === 1) {
|
|
3714
4122
|
throw new Error(
|
|
@@ -3717,54 +4125,431 @@ async function casWriteOnce(ctx2, mutate) {
|
|
|
3717
4125
|
}
|
|
3718
4126
|
continue;
|
|
3719
4127
|
}
|
|
3720
|
-
await atomicWrite(
|
|
3721
|
-
|
|
3722
|
-
serialiseMirror(result, after.rev + 1, after.zone || localZone())
|
|
3723
|
-
);
|
|
3724
|
-
return summary;
|
|
4128
|
+
await atomicWrite(
|
|
4129
|
+
ctx.path,
|
|
4130
|
+
serialiseMirror(result, after.rev + 1, after.zone || localZone())
|
|
4131
|
+
);
|
|
4132
|
+
return summary;
|
|
4133
|
+
}
|
|
4134
|
+
throw new Error("unreachable");
|
|
4135
|
+
} finally {
|
|
4136
|
+
await rm(lock, { force: true });
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
var writeQueue = Promise.resolve();
|
|
4140
|
+
function casWrite(ctx, mutate) {
|
|
4141
|
+
const run = writeQueue.then(() => casWriteOnce(ctx, mutate));
|
|
4142
|
+
writeQueue = run.then(
|
|
4143
|
+
() => void 0,
|
|
4144
|
+
() => void 0
|
|
4145
|
+
);
|
|
4146
|
+
return run;
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
// mcp/cloud.ts
|
|
4150
|
+
function credentialsPath(env = process.env) {
|
|
4151
|
+
if (env.CIRCAL_CLOUD_CREDENTIALS) return env.CIRCAL_CLOUD_CREDENTIALS;
|
|
4152
|
+
const home = homedir();
|
|
4153
|
+
if (process.platform === "darwin")
|
|
4154
|
+
return join2(home, "Library", "Application Support", "circal", "cloud.json");
|
|
4155
|
+
if (process.platform === "win32")
|
|
4156
|
+
return join2(env.APPDATA || join2(home, "AppData", "Roaming"), "circal", "cloud.json");
|
|
4157
|
+
return join2(env.XDG_CONFIG_HOME || join2(home, ".config"), "circal", "cloud.json");
|
|
4158
|
+
}
|
|
4159
|
+
async function readCredentials(path) {
|
|
4160
|
+
let text;
|
|
4161
|
+
try {
|
|
4162
|
+
text = await readFile2(path, "utf8");
|
|
4163
|
+
} catch {
|
|
4164
|
+
return null;
|
|
4165
|
+
}
|
|
4166
|
+
let parsed;
|
|
4167
|
+
try {
|
|
4168
|
+
parsed = JSON.parse(text);
|
|
4169
|
+
} catch {
|
|
4170
|
+
return null;
|
|
4171
|
+
}
|
|
4172
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
4173
|
+
const record = parsed;
|
|
4174
|
+
const strings = [
|
|
4175
|
+
"url",
|
|
4176
|
+
"key",
|
|
4177
|
+
"email",
|
|
4178
|
+
"user",
|
|
4179
|
+
"salt",
|
|
4180
|
+
"aesKey",
|
|
4181
|
+
"accessToken",
|
|
4182
|
+
"refreshToken"
|
|
4183
|
+
];
|
|
4184
|
+
for (const field of strings) if (typeof record[field] !== "string" || !record[field]) return null;
|
|
4185
|
+
if (typeof record.expiresAt !== "number") return null;
|
|
4186
|
+
return record;
|
|
4187
|
+
}
|
|
4188
|
+
async function writeCredentials(path, creds) {
|
|
4189
|
+
await mkdir(join2(path, ".."), { recursive: true });
|
|
4190
|
+
await writeFile(path, JSON.stringify(creds, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
4191
|
+
await chmod(path, 384);
|
|
4192
|
+
}
|
|
4193
|
+
function readSession(value) {
|
|
4194
|
+
if (typeof value !== "object" || value === null) return null;
|
|
4195
|
+
const body = value;
|
|
4196
|
+
const user = body.user;
|
|
4197
|
+
if (typeof body.access_token !== "string" || typeof body.refresh_token !== "string") return null;
|
|
4198
|
+
if (typeof user?.id !== "string") return null;
|
|
4199
|
+
const expiresAt = typeof body.expires_at === "number" ? body.expires_at : Math.floor(Date.now() / 1e3) + (typeof body.expires_in === "number" ? body.expires_in : 3600);
|
|
4200
|
+
return {
|
|
4201
|
+
accessToken: body.access_token,
|
|
4202
|
+
refreshToken: body.refresh_token,
|
|
4203
|
+
expiresAt,
|
|
4204
|
+
user: user.id,
|
|
4205
|
+
email: typeof user.email === "string" ? user.email : ""
|
|
4206
|
+
};
|
|
4207
|
+
}
|
|
4208
|
+
async function apiError(response, doing) {
|
|
4209
|
+
let detail = "";
|
|
4210
|
+
try {
|
|
4211
|
+
const body = await response.json();
|
|
4212
|
+
detail = String(body.msg ?? body.message ?? body.error_description ?? body.error ?? "");
|
|
4213
|
+
} catch {
|
|
4214
|
+
}
|
|
4215
|
+
return new Error(`${doing} failed (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
4216
|
+
}
|
|
4217
|
+
async function authRequest(url, key, path, body, doing) {
|
|
4218
|
+
const response = await fetch(`${url}/auth/v1/${path}`, {
|
|
4219
|
+
method: "POST",
|
|
4220
|
+
headers: { apikey: key, "Content-Type": "application/json" },
|
|
4221
|
+
body: JSON.stringify(body)
|
|
4222
|
+
});
|
|
4223
|
+
if (!response.ok) throw await apiError(response, doing);
|
|
4224
|
+
return response.json();
|
|
4225
|
+
}
|
|
4226
|
+
async function requestCode(url, key, email) {
|
|
4227
|
+
await authRequest(url, key, "otp", { email, create_user: true }, "requesting a sign-in code");
|
|
4228
|
+
}
|
|
4229
|
+
async function verifyCode(url, key, email, token) {
|
|
4230
|
+
const body = await authRequest(
|
|
4231
|
+
url,
|
|
4232
|
+
key,
|
|
4233
|
+
"verify",
|
|
4234
|
+
{ type: "email", email, token },
|
|
4235
|
+
"verifying the code"
|
|
4236
|
+
);
|
|
4237
|
+
const session = readSession(body);
|
|
4238
|
+
if (!session) throw new Error("the sign-in answer did not carry a session");
|
|
4239
|
+
return session;
|
|
4240
|
+
}
|
|
4241
|
+
async function refreshSession(url, key, refreshToken) {
|
|
4242
|
+
const body = await authRequest(
|
|
4243
|
+
url,
|
|
4244
|
+
key,
|
|
4245
|
+
"token?grant_type=refresh_token",
|
|
4246
|
+
{ refresh_token: refreshToken },
|
|
4247
|
+
"refreshing the session"
|
|
4248
|
+
);
|
|
4249
|
+
const session = readSession(body);
|
|
4250
|
+
if (!session) throw new Error("the refresh answer did not carry a session");
|
|
4251
|
+
return session;
|
|
4252
|
+
}
|
|
4253
|
+
var EXPIRY_SLACK_S = 60;
|
|
4254
|
+
var CloudClient = class {
|
|
4255
|
+
constructor(creds, path) {
|
|
4256
|
+
this.creds = creds;
|
|
4257
|
+
this.path = path;
|
|
4258
|
+
}
|
|
4259
|
+
creds;
|
|
4260
|
+
path;
|
|
4261
|
+
get email() {
|
|
4262
|
+
return this.creds.email;
|
|
4263
|
+
}
|
|
4264
|
+
/**
|
|
4265
|
+
* A live access token, refreshed when near expiry. GoTrue *rotates*
|
|
4266
|
+
* refresh tokens, so a successful refresh is persisted immediately —
|
|
4267
|
+
* losing the rotation means the next launch's token is already spent
|
|
4268
|
+
* and the user is sent back through `login` for no crime of theirs.
|
|
4269
|
+
* The persistence itself is best-effort, `rememberRev`'s reason: the
|
|
4270
|
+
* refresh has already succeeded, and failing the user's actual request
|
|
4271
|
+
* over bookkeeping is the wrong way round.
|
|
4272
|
+
*/
|
|
4273
|
+
async token() {
|
|
4274
|
+
if (this.creds.expiresAt - EXPIRY_SLACK_S > Date.now() / 1e3) return this.creds.accessToken;
|
|
4275
|
+
const session = await refreshSession(this.creds.url, this.creds.key, this.creds.refreshToken);
|
|
4276
|
+
this.creds = {
|
|
4277
|
+
...this.creds,
|
|
4278
|
+
accessToken: session.accessToken,
|
|
4279
|
+
refreshToken: session.refreshToken,
|
|
4280
|
+
expiresAt: session.expiresAt
|
|
4281
|
+
};
|
|
4282
|
+
try {
|
|
4283
|
+
await writeCredentials(this.path, this.creds);
|
|
4284
|
+
} catch {
|
|
4285
|
+
}
|
|
4286
|
+
return this.creds.accessToken;
|
|
4287
|
+
}
|
|
4288
|
+
async rest(path, init) {
|
|
4289
|
+
const token = await this.token();
|
|
4290
|
+
return fetch(`${this.creds.url}/rest/v1/${path}`, {
|
|
4291
|
+
...init,
|
|
4292
|
+
headers: {
|
|
4293
|
+
apikey: this.creds.key,
|
|
4294
|
+
Authorization: `Bearer ${token}`,
|
|
4295
|
+
"Content-Type": "application/json",
|
|
4296
|
+
...init.headers ?? {}
|
|
4297
|
+
}
|
|
4298
|
+
});
|
|
4299
|
+
}
|
|
4300
|
+
/** The row, or `null` when the account has none yet. RLS scopes the select
|
|
4301
|
+
* to the signed-in user's own row, so there is no filter to get wrong. */
|
|
4302
|
+
async fetchRow() {
|
|
4303
|
+
const response = await this.rest("documents?select=rev,version,zone,payload", {
|
|
4304
|
+
method: "GET"
|
|
4305
|
+
});
|
|
4306
|
+
if (!response.ok) throw await apiError(response, "reading the cloud copy");
|
|
4307
|
+
const rows = await response.json();
|
|
4308
|
+
return readCloudRow(rows[0]);
|
|
4309
|
+
}
|
|
4310
|
+
/**
|
|
4311
|
+
* The compare-and-swap: `update … where rev = expected`, and the row count
|
|
4312
|
+
* answers whether we won. `Prefer: return=representation` is not decoration
|
|
4313
|
+
* — without it PostgREST returns nothing to count (the browser's `replace`
|
|
4314
|
+
* makes the same point with `.select()`). No insert branch on purpose: this
|
|
4315
|
+
* client never creates the row, so "no row" and "somebody moved it" can
|
|
4316
|
+
* share the miss without ambiguity — the caller re-reads either way.
|
|
4317
|
+
*/
|
|
4318
|
+
async casReplace(expected, next, zone, payload) {
|
|
4319
|
+
const response = await this.rest(`documents?rev=eq.${expected}`, {
|
|
4320
|
+
method: "PATCH",
|
|
4321
|
+
headers: { Prefer: "return=representation" },
|
|
4322
|
+
body: JSON.stringify({ rev: next, version: MIRROR_VERSION, zone, payload })
|
|
4323
|
+
});
|
|
4324
|
+
if (!response.ok) throw await apiError(response, "writing the cloud copy");
|
|
4325
|
+
const rows = await response.json();
|
|
4326
|
+
return rows.length === 1;
|
|
4327
|
+
}
|
|
4328
|
+
};
|
|
4329
|
+
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.";
|
|
4330
|
+
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.";
|
|
4331
|
+
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.";
|
|
4332
|
+
function cloudStore(ctx) {
|
|
4333
|
+
const client = new CloudClient(ctx.credentials, ctx.path);
|
|
4334
|
+
const storedSalt = base64UrlToBytes(ctx.credentials.salt);
|
|
4335
|
+
const storedKey = base64UrlToBytes(ctx.credentials.aesKey);
|
|
4336
|
+
if (!storedSalt || !storedKey) {
|
|
4337
|
+
throw new Error(`${ctx.path} is damaged \u2014 run \`circal-mcp login\` to rewrite it.`);
|
|
4338
|
+
}
|
|
4339
|
+
const salt = storedSalt;
|
|
4340
|
+
const vaultReady = restoreKeyed(storedKey, salt).then(vaultOf);
|
|
4341
|
+
async function openRow(row) {
|
|
4342
|
+
const rowSalt = saltOf(row.payload);
|
|
4343
|
+
if (!rowSalt || rowSalt.length !== salt.length || !rowSalt.every((b, i) => b === salt[i])) {
|
|
4344
|
+
throw new RefusalError(RESEALED_HERE);
|
|
4345
|
+
}
|
|
4346
|
+
let text;
|
|
4347
|
+
try {
|
|
4348
|
+
text = await (await vaultReady).open(row.payload);
|
|
4349
|
+
} catch (error) {
|
|
4350
|
+
if (error instanceof WrongPassphrase) throw new RefusalError(UNOPENABLE);
|
|
4351
|
+
if (error instanceof UnreadablePayload)
|
|
4352
|
+
throw new RefusalError(
|
|
4353
|
+
"The cloud copy's payload is not a vault this build can read. Nothing was written."
|
|
4354
|
+
);
|
|
4355
|
+
throw error;
|
|
4356
|
+
}
|
|
4357
|
+
const contents = readMirrorFile(text);
|
|
4358
|
+
if (!contents) {
|
|
4359
|
+
throw new Error("the cloud payload decrypted, but not into a circal document");
|
|
4360
|
+
}
|
|
4361
|
+
return { ...contents, rev: row.rev, zone: row.zone || contents.zone };
|
|
4362
|
+
}
|
|
4363
|
+
async function loadOnce() {
|
|
4364
|
+
const row = await client.fetchRow();
|
|
4365
|
+
if (!row) throw new RefusalError(NO_ROW);
|
|
4366
|
+
const contents = await openRow(row);
|
|
4367
|
+
return { contents, warning: zoneWarning(contents) };
|
|
4368
|
+
}
|
|
4369
|
+
async function writeOnce(mutate) {
|
|
4370
|
+
let firstRev = null;
|
|
4371
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4372
|
+
const row = await client.fetchRow();
|
|
4373
|
+
if (!row) throw new RefusalError(NO_ROW);
|
|
4374
|
+
const contents = await openRow(row);
|
|
4375
|
+
assertVersionAllowsMutation(contents);
|
|
4376
|
+
assertZoneAllowsMutation(contents, ctx.env);
|
|
4377
|
+
const { result, summary } = mutate(contents);
|
|
4378
|
+
const zone = row.zone || localZone();
|
|
4379
|
+
const next = row.rev + 1;
|
|
4380
|
+
const payload = await (await vaultReady).seal(serialiseMirror(result, next, zone));
|
|
4381
|
+
if (await client.casReplace(row.rev, next, zone, payload)) return summary;
|
|
4382
|
+
if (firstRev === null) firstRev = row.rev;
|
|
4383
|
+
else {
|
|
4384
|
+
throw new Error(
|
|
4385
|
+
`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.`
|
|
4386
|
+
);
|
|
4387
|
+
}
|
|
3725
4388
|
}
|
|
3726
4389
|
throw new Error("unreachable");
|
|
3727
|
-
} finally {
|
|
3728
|
-
await rm(lock, { force: true });
|
|
3729
4390
|
}
|
|
4391
|
+
let writeQueue2 = Promise.resolve();
|
|
4392
|
+
return {
|
|
4393
|
+
transport: `cloud (${ctx.credentials.email || "signed in"})`,
|
|
4394
|
+
load: loadOnce,
|
|
4395
|
+
write(mutate) {
|
|
4396
|
+
const run = writeQueue2.then(() => writeOnce(mutate));
|
|
4397
|
+
writeQueue2 = run.then(
|
|
4398
|
+
() => void 0,
|
|
4399
|
+
() => void 0
|
|
4400
|
+
);
|
|
4401
|
+
return run;
|
|
4402
|
+
}
|
|
4403
|
+
};
|
|
3730
4404
|
}
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
const
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
4405
|
+
function ask(question, hidden = false) {
|
|
4406
|
+
const muted = { current: false };
|
|
4407
|
+
const output = new Writable({
|
|
4408
|
+
write(chunk, _enc, done) {
|
|
4409
|
+
if (!muted.current) process.stdout.write(chunk);
|
|
4410
|
+
done();
|
|
4411
|
+
}
|
|
4412
|
+
});
|
|
4413
|
+
const rl = createInterface({ input: process.stdin, output, terminal: true });
|
|
4414
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
4415
|
+
rl.question(question, (answer) => {
|
|
4416
|
+
muted.current = false;
|
|
4417
|
+
if (hidden) process.stdout.write("\n");
|
|
4418
|
+
rl.close();
|
|
4419
|
+
resolve(answer.trim());
|
|
4420
|
+
});
|
|
4421
|
+
muted.current = hidden;
|
|
4422
|
+
return promise;
|
|
4423
|
+
}
|
|
4424
|
+
function flagValue(argv2, flag) {
|
|
4425
|
+
const index = argv2.indexOf(flag);
|
|
4426
|
+
return index >= 0 ? argv2[index + 1] : void 0;
|
|
4427
|
+
}
|
|
4428
|
+
var DEFAULT_SITE = "https://circal.day";
|
|
4429
|
+
function readProject(value) {
|
|
4430
|
+
if (typeof value !== "object" || value === null) return null;
|
|
4431
|
+
const body = value;
|
|
4432
|
+
if (typeof body.url !== "string" || !body.url) return null;
|
|
4433
|
+
if (typeof body.key !== "string" || !body.key) return null;
|
|
4434
|
+
return { url: body.url.replace(/\/+$/, ""), key: body.key };
|
|
4435
|
+
}
|
|
4436
|
+
async function resolveProject(argv2, env) {
|
|
4437
|
+
const url = (flagValue(argv2, "--url") || env.PUBLIC_SUPABASE_URL || "").replace(/\/+$/, "");
|
|
4438
|
+
const key = flagValue(argv2, "--key") || env.PUBLIC_SUPABASE_PUBLISHABLE_KEY || "";
|
|
4439
|
+
if (url && key) return { url, key };
|
|
4440
|
+
if (url || key) {
|
|
4441
|
+
throw new Error(
|
|
4442
|
+
"circal-mcp login was given half a project: --url and --key (or the two PUBLIC_SUPABASE_* variables) travel together."
|
|
4443
|
+
);
|
|
4444
|
+
}
|
|
4445
|
+
const site = (flagValue(argv2, "--site") || DEFAULT_SITE).replace(/\/+$/, "");
|
|
4446
|
+
let response;
|
|
4447
|
+
try {
|
|
4448
|
+
response = await fetch(`${site}/mcp.json`);
|
|
4449
|
+
} catch (error) {
|
|
4450
|
+
throw new Error(`could not reach ${site} to ask which project it uses`, { cause: error });
|
|
4451
|
+
}
|
|
4452
|
+
if (!response.ok) {
|
|
4453
|
+
throw new Error(
|
|
4454
|
+
`${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).`
|
|
4455
|
+
);
|
|
4456
|
+
}
|
|
4457
|
+
const project = readProject(await response.json().catch(() => null));
|
|
4458
|
+
if (!project) {
|
|
4459
|
+
throw new Error(
|
|
4460
|
+
`${site} does not offer a cloud copy (its /mcp.json names no project). Pass --url and --key if you run your own.`
|
|
4461
|
+
);
|
|
4462
|
+
}
|
|
4463
|
+
return project;
|
|
4464
|
+
}
|
|
4465
|
+
async function runLogin(argv2, env) {
|
|
4466
|
+
const { url, key } = await resolveProject(argv2, env);
|
|
4467
|
+
const email = await ask("Email: ");
|
|
4468
|
+
if (!email) throw new Error("an email address is required");
|
|
4469
|
+
await requestCode(url, key, email);
|
|
4470
|
+
const code = await ask("Sign-in code (from the email): ");
|
|
4471
|
+
const session = await verifyCode(url, key, email, code);
|
|
4472
|
+
const client = new CloudClient(
|
|
4473
|
+
{
|
|
4474
|
+
url,
|
|
4475
|
+
key,
|
|
4476
|
+
email,
|
|
4477
|
+
user: session.user,
|
|
4478
|
+
salt: "",
|
|
4479
|
+
aesKey: "",
|
|
4480
|
+
accessToken: session.accessToken,
|
|
4481
|
+
refreshToken: session.refreshToken,
|
|
4482
|
+
expiresAt: session.expiresAt
|
|
4483
|
+
},
|
|
4484
|
+
credentialsPath(env)
|
|
3737
4485
|
);
|
|
3738
|
-
|
|
4486
|
+
const row = await client.fetchRow();
|
|
4487
|
+
if (!row) throw new Error(NO_ROW);
|
|
4488
|
+
const foundSalt = saltOf(row.payload);
|
|
4489
|
+
if (!foundSalt) throw new Error("the cloud copy's payload is not a vault this build can read");
|
|
4490
|
+
const salt = new Uint8Array(foundSalt);
|
|
4491
|
+
const passphrase = await ask("Passphrase: ", true);
|
|
4492
|
+
const rawKey = await deriveBits(passphrase, salt);
|
|
4493
|
+
try {
|
|
4494
|
+
await vaultOf(await restoreKeyed(rawKey, salt)).open(row.payload);
|
|
4495
|
+
} catch (error) {
|
|
4496
|
+
if (error instanceof WrongPassphrase) throw new Error(LOCKED, { cause: error });
|
|
4497
|
+
throw error;
|
|
4498
|
+
}
|
|
4499
|
+
const path = credentialsPath(env);
|
|
4500
|
+
await writeCredentials(path, {
|
|
4501
|
+
url,
|
|
4502
|
+
key,
|
|
4503
|
+
email: session.email || email,
|
|
4504
|
+
user: session.user,
|
|
4505
|
+
salt: bytesToBase64Url(salt),
|
|
4506
|
+
aesKey: bytesToBase64Url(rawKey),
|
|
4507
|
+
accessToken: session.accessToken,
|
|
4508
|
+
refreshToken: session.refreshToken,
|
|
4509
|
+
expiresAt: session.expiresAt
|
|
4510
|
+
});
|
|
4511
|
+
console.log(`
|
|
4512
|
+
Signed in as ${session.email || email}; the cloud copy opens.`);
|
|
4513
|
+
console.log(`Credentials written to ${path} (0600 \u2014 the derived key, never the passphrase).`);
|
|
4514
|
+
console.log(`
|
|
4515
|
+
Serve it with:
|
|
4516
|
+
|
|
4517
|
+
circal-mcp --cloud
|
|
4518
|
+
`);
|
|
4519
|
+
}
|
|
4520
|
+
async function runLogout(env) {
|
|
4521
|
+
const path = credentialsPath(env);
|
|
4522
|
+
await rm2(path, { force: true });
|
|
4523
|
+
console.log(`Removed ${path}. This machine can no longer open the cloud copy.`);
|
|
3739
4524
|
}
|
|
3740
4525
|
|
|
3741
4526
|
// mcp/find.ts
|
|
3742
|
-
import { readFile as
|
|
3743
|
-
import { homedir } from "node:os";
|
|
3744
|
-
import { join as
|
|
4527
|
+
import { readFile as readFile3, readdir, stat as stat2 } from "node:fs/promises";
|
|
4528
|
+
import { homedir as homedir2 } from "node:os";
|
|
4529
|
+
import { join as join3 } from "node:path";
|
|
3745
4530
|
var SKIP_DIR_NAMES = { node_modules: true, Library: true, ".git": true };
|
|
3746
4531
|
var MAX_FILE_BYTES = 32 * 1024 * 1024;
|
|
3747
4532
|
function roots(home) {
|
|
3748
4533
|
return [
|
|
3749
4534
|
home,
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
4535
|
+
join3(home, "Documents"),
|
|
4536
|
+
join3(home, "Desktop"),
|
|
4537
|
+
join3(home, "Downloads"),
|
|
4538
|
+
join3(home, "Library", "Mobile Documents", "com~apple~CloudDocs"),
|
|
3754
4539
|
// iCloud Drive (Documents)
|
|
3755
|
-
|
|
3756
|
-
|
|
4540
|
+
join3(home, "Dropbox"),
|
|
4541
|
+
join3(home, "Sync")
|
|
3757
4542
|
];
|
|
3758
4543
|
}
|
|
3759
4544
|
function appDefault(home) {
|
|
3760
|
-
return
|
|
4545
|
+
return join3(home, "Library", "Application Support", "circal", "calendar.json");
|
|
3761
4546
|
}
|
|
3762
4547
|
async function jsonFilesUnder(root) {
|
|
3763
4548
|
const found = [];
|
|
3764
4549
|
const top = await listDir(root);
|
|
3765
4550
|
for (const entry of top) {
|
|
3766
4551
|
if (entry.name.startsWith(".")) continue;
|
|
3767
|
-
const full =
|
|
4552
|
+
const full = join3(root, entry.name);
|
|
3768
4553
|
if (entry.isFile()) {
|
|
3769
4554
|
if (entry.name.toLowerCase().endsWith(".json")) found.push(full);
|
|
3770
4555
|
continue;
|
|
@@ -3772,39 +4557,39 @@ async function jsonFilesUnder(root) {
|
|
|
3772
4557
|
if (!entry.isDirectory() || SKIP_DIR_NAMES[entry.name]) continue;
|
|
3773
4558
|
for (const sub of await listDir(full)) {
|
|
3774
4559
|
if (sub.name.startsWith(".") || !sub.isFile()) continue;
|
|
3775
|
-
if (sub.name.toLowerCase().endsWith(".json")) found.push(
|
|
4560
|
+
if (sub.name.toLowerCase().endsWith(".json")) found.push(join3(full, sub.name));
|
|
3776
4561
|
}
|
|
3777
4562
|
}
|
|
3778
4563
|
return found;
|
|
3779
4564
|
}
|
|
3780
|
-
async function listDir(
|
|
4565
|
+
async function listDir(path) {
|
|
3781
4566
|
try {
|
|
3782
|
-
return await readdir(
|
|
4567
|
+
return await readdir(path, { withFileTypes: true });
|
|
3783
4568
|
} catch {
|
|
3784
4569
|
return [];
|
|
3785
4570
|
}
|
|
3786
4571
|
}
|
|
3787
|
-
async function asMirror(
|
|
4572
|
+
async function asMirror(path) {
|
|
3788
4573
|
try {
|
|
3789
|
-
const info = await stat2(
|
|
4574
|
+
const info = await stat2(path);
|
|
3790
4575
|
if (!info.isFile() || info.size > MAX_FILE_BYTES) return null;
|
|
3791
|
-
const text = await
|
|
4576
|
+
const text = await readFile3(path, "utf8");
|
|
3792
4577
|
const contents = readMirrorFile(text);
|
|
3793
|
-
return contents ? { path
|
|
4578
|
+
return contents ? { path, contents } : null;
|
|
3794
4579
|
} catch {
|
|
3795
4580
|
return null;
|
|
3796
4581
|
}
|
|
3797
4582
|
}
|
|
3798
|
-
async function runFind(home =
|
|
4583
|
+
async function runFind(home = homedir2()) {
|
|
3799
4584
|
const searchRoots = roots(home);
|
|
3800
4585
|
const candidates = /* @__PURE__ */ new Set();
|
|
3801
4586
|
candidates.add(appDefault(home));
|
|
3802
4587
|
for (const root of searchRoots) {
|
|
3803
|
-
for (const
|
|
4588
|
+
for (const path of await jsonFilesUnder(root)) candidates.add(path);
|
|
3804
4589
|
}
|
|
3805
4590
|
const hits = [];
|
|
3806
|
-
for (const
|
|
3807
|
-
const hit = await asMirror(
|
|
4591
|
+
for (const path of [...candidates].sort()) {
|
|
4592
|
+
const hit = await asMirror(path);
|
|
3808
4593
|
if (hit) hits.push(hit);
|
|
3809
4594
|
}
|
|
3810
4595
|
if (hits.length === 0) {
|
|
@@ -3939,22 +4724,47 @@ function requireScope(event, scope) {
|
|
|
3939
4724
|
}
|
|
3940
4725
|
return scope;
|
|
3941
4726
|
}
|
|
3942
|
-
function
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
4727
|
+
function findCalendar(ref, calendars) {
|
|
4728
|
+
const byId = calendars.find((c) => c.id === ref);
|
|
4729
|
+
if (byId) return byId;
|
|
4730
|
+
const needle = ref.trim().toLowerCase();
|
|
4731
|
+
const matches = needle ? calendars.filter((c) => c.name.toLowerCase().startsWith(needle)) : [];
|
|
4732
|
+
if (matches.length === 1) return matches[0];
|
|
4733
|
+
if (matches.length > 1) {
|
|
4734
|
+
throw new RefusalError(
|
|
4735
|
+
`"${ref}" names ${matches.length} calendars (${matches.map((c) => c.name).join(", ")}) \u2014 use an id from list_calendars`
|
|
4736
|
+
);
|
|
4737
|
+
}
|
|
4738
|
+
throw new RefusalError(`no calendar with id or name "${ref}"`);
|
|
4739
|
+
}
|
|
4740
|
+
function requireWritable(calendar) {
|
|
4741
|
+
if (calendar.feed) {
|
|
4742
|
+
throw new RefusalError(
|
|
4743
|
+
`"${calendar.name}" is a subscription \u2014 its events are pulled from a feed, never written to`
|
|
4744
|
+
);
|
|
3952
4745
|
}
|
|
4746
|
+
return calendar;
|
|
4747
|
+
}
|
|
4748
|
+
function resolveCalendar(calendarId, calendars, settings, pin) {
|
|
4749
|
+
if (calendarId) return requireWritable(findCalendar(calendarId, calendars));
|
|
4750
|
+
if (pin) return requireWritable(findCalendar(pin, calendars));
|
|
3953
4751
|
const id = defaultCalendarId(calendars, settings.defaultCalendar);
|
|
3954
4752
|
const found = calendars.find((c) => c.id === id);
|
|
3955
4753
|
if (!found) throw new RefusalError("there is no calendar to put this on \u2014 create one first");
|
|
3956
4754
|
return found;
|
|
3957
4755
|
}
|
|
4756
|
+
function defaultedCalendar(calendarId, pin) {
|
|
4757
|
+
return calendarId === void 0 && !pin;
|
|
4758
|
+
}
|
|
4759
|
+
function scopedToCalendar(contents, ref) {
|
|
4760
|
+
if (!ref) return contents;
|
|
4761
|
+
const calendar = findCalendar(ref, contents.calendars);
|
|
4762
|
+
return {
|
|
4763
|
+
...contents,
|
|
4764
|
+
events: contents.events.filter((e) => e.calendarId === calendar.id),
|
|
4765
|
+
tasks: contents.tasks.filter((t) => t.calendarId === calendar.id)
|
|
4766
|
+
};
|
|
4767
|
+
}
|
|
3958
4768
|
function refuseIfFeed(event, calendars) {
|
|
3959
4769
|
const calendar = calendars.find((c) => c.id === event.calendarId);
|
|
3960
4770
|
if (calendar?.feed) {
|
|
@@ -3963,49 +4773,70 @@ function refuseIfFeed(event, calendars) {
|
|
|
3963
4773
|
);
|
|
3964
4774
|
}
|
|
3965
4775
|
}
|
|
3966
|
-
function buildRecurrence(input) {
|
|
4776
|
+
function buildRecurrence(input, zone = "") {
|
|
3967
4777
|
if (!input) return { ...NEVER };
|
|
3968
|
-
const
|
|
4778
|
+
const boundary = input.until ? addDays(fromDayKey(input.until), 1) : null;
|
|
4779
|
+
const until = boundary ? (zone ? instantOfWall(boundary, zone) : boundary.getTime()) - 1 : 0;
|
|
3969
4780
|
return readRecurrence({ ...input, until, exdates: [], rdates: [] });
|
|
3970
4781
|
}
|
|
3971
|
-
function
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
4782
|
+
function spanInstant(day, minutes, zone) {
|
|
4783
|
+
return zone ? instantOfWall(atMinutes(day, minutes), zone) : atMinutes(day, minutes).getTime();
|
|
4784
|
+
}
|
|
4785
|
+
function zoneFrom(value) {
|
|
4786
|
+
if (value === "") return "";
|
|
4787
|
+
if (!isZone(value)) {
|
|
4788
|
+
throw new RefusalError(
|
|
4789
|
+
`"${value}" is not an IANA time zone this runtime knows \u2014 try "Asia/Seoul"`
|
|
4790
|
+
);
|
|
4791
|
+
}
|
|
4792
|
+
return value;
|
|
4793
|
+
}
|
|
4794
|
+
function leadFrom(value) {
|
|
4795
|
+
if (value === "inherit") return REMIND_INHERIT;
|
|
4796
|
+
if (value === "silent") return REMIND_SILENT;
|
|
4797
|
+
if (readRemindLead(value) !== value) {
|
|
4798
|
+
throw new RefusalError(
|
|
4799
|
+
`"remindLead" must be whole minutes, 0 to ${REMIND_LEAD_MAX} \u2014 got ${value}`
|
|
4800
|
+
);
|
|
3976
4801
|
}
|
|
4802
|
+
return value;
|
|
4803
|
+
}
|
|
4804
|
+
function resolveSpanForCreate(input, day, defaultDuration, zone) {
|
|
3977
4805
|
const endIsDay = input.end !== void 0 && DATE_KEY_RE.test(input.end);
|
|
3978
4806
|
const noTimeGiven = input.start === void 0 && (input.end === void 0 || endIsDay) && input.duration === void 0;
|
|
3979
4807
|
const allDay = input.allDay === true || input.allDay === void 0 && noTimeGiven;
|
|
3980
4808
|
if (allDay) {
|
|
4809
|
+
if (zone) {
|
|
4810
|
+
throw new RefusalError(
|
|
4811
|
+
'an all-day event carries no clock to zone \u2014 drop "zone" or give a timed "start"'
|
|
4812
|
+
);
|
|
4813
|
+
}
|
|
3981
4814
|
const days = input.end ? Math.max(1, daysBetween(day, fromDayKey(input.end)) + 1) : 1;
|
|
3982
4815
|
return {
|
|
3983
4816
|
start: startOfDay(day).getTime(),
|
|
3984
4817
|
end: addDays(startOfDay(day), days).getTime(),
|
|
3985
|
-
allDay: true
|
|
3986
|
-
task: false
|
|
4818
|
+
allDay: true
|
|
3987
4819
|
};
|
|
3988
4820
|
}
|
|
3989
4821
|
if (input.start === void 0) throw new RefusalError('a timed event needs "start" (HH:MM)');
|
|
3990
4822
|
const startMin = parseTime(input.start, "start");
|
|
3991
4823
|
const endMin = input.end !== void 0 ? parseTime(input.end, "end") : startMin + (input.duration ?? defaultDuration);
|
|
3992
4824
|
return {
|
|
3993
|
-
start:
|
|
3994
|
-
end:
|
|
3995
|
-
allDay: false
|
|
3996
|
-
task: false
|
|
4825
|
+
start: spanInstant(day, startMin, zone),
|
|
4826
|
+
end: spanInstant(day, endMin, zone),
|
|
4827
|
+
allDay: false
|
|
3997
4828
|
};
|
|
3998
4829
|
}
|
|
3999
|
-
function resolveSpanForPatch(input, base, anchorMs) {
|
|
4000
|
-
const
|
|
4001
|
-
const
|
|
4002
|
-
if (task) {
|
|
4003
|
-
const startMin2 = input.start !== void 0 ? parseTime(input.start, "start") : minutesOfDay(new Date(anchorMs));
|
|
4004
|
-
const at = atMinutes(day, startMin2).getTime();
|
|
4005
|
-
return { start: at, end: at, allDay: false, task: true };
|
|
4006
|
-
}
|
|
4830
|
+
function resolveSpanForPatch(input, base, anchorMs, zone) {
|
|
4831
|
+
const anchorWall = base.tzid ? wallClockIn(anchorMs, base.tzid) : new Date(anchorMs);
|
|
4832
|
+
const day = input.date ? parseDay(input.date) : startOfDay(anchorWall);
|
|
4007
4833
|
const allDay = input.allDay ?? base.allDay;
|
|
4008
4834
|
if (allDay) {
|
|
4835
|
+
if (input.zone) {
|
|
4836
|
+
throw new RefusalError(
|
|
4837
|
+
'an all-day event carries no clock to zone \u2014 drop "zone" or give a timed "start"'
|
|
4838
|
+
);
|
|
4839
|
+
}
|
|
4009
4840
|
const baseDays = Math.max(
|
|
4010
4841
|
1,
|
|
4011
4842
|
daysBetween(startOfDay(new Date(base.start)), startOfDay(new Date(base.end)))
|
|
@@ -4014,94 +4845,97 @@ function resolveSpanForPatch(input, base, anchorMs) {
|
|
|
4014
4845
|
return {
|
|
4015
4846
|
start: startOfDay(day).getTime(),
|
|
4016
4847
|
end: addDays(startOfDay(day), days).getTime(),
|
|
4017
|
-
allDay: true
|
|
4018
|
-
task: false
|
|
4848
|
+
allDay: true
|
|
4019
4849
|
};
|
|
4020
4850
|
}
|
|
4021
4851
|
const baseDurationMin = Math.max(1, Math.round((base.end - base.start) / 6e4));
|
|
4022
|
-
const startMin = input.start !== void 0 ? parseTime(input.start, "start") : minutesOfDay(
|
|
4852
|
+
const startMin = input.start !== void 0 ? parseTime(input.start, "start") : minutesOfDay(anchorWall);
|
|
4023
4853
|
const endMin = input.end !== void 0 ? parseTime(input.end, "end") : startMin + (input.duration ?? baseDurationMin);
|
|
4024
4854
|
return {
|
|
4025
|
-
start:
|
|
4026
|
-
end:
|
|
4027
|
-
allDay: false
|
|
4028
|
-
task: false
|
|
4855
|
+
start: spanInstant(day, startMin, zone),
|
|
4856
|
+
end: spanInstant(day, endMin, zone),
|
|
4857
|
+
allDay: false
|
|
4029
4858
|
};
|
|
4030
4859
|
}
|
|
4031
4860
|
function validate(draft) {
|
|
4032
4861
|
const validated = readEvent({ ...draft, id: "pending" });
|
|
4033
4862
|
if (!validated)
|
|
4034
|
-
throw new RefusalError("the assembled event failed validation (check start
|
|
4863
|
+
throw new RefusalError("the assembled event failed validation (check start and end)");
|
|
4035
4864
|
const { id: _drop, ...rest } = validated;
|
|
4036
4865
|
return rest;
|
|
4037
4866
|
}
|
|
4038
|
-
function buildEventRecord(input, calendars, settings) {
|
|
4867
|
+
function buildEventRecord(input, calendars, settings, pin) {
|
|
4039
4868
|
if (!input.date) throw new RefusalError('an event needs "date" (YYYY-MM-DD)');
|
|
4040
4869
|
const day = parseDay(input.date);
|
|
4041
|
-
const calendar = resolveCalendar(input.calendarId, calendars, settings);
|
|
4042
|
-
const
|
|
4870
|
+
const calendar = resolveCalendar(input.calendarId, calendars, settings, pin);
|
|
4871
|
+
const zone = input.zone ? zoneFrom(input.zone) : "";
|
|
4872
|
+
const span = resolveSpanForCreate(input, day, settings.defaultDuration, zone);
|
|
4043
4873
|
return validate({
|
|
4044
4874
|
title: input.title ?? "",
|
|
4045
4875
|
calendarId: calendar.id,
|
|
4046
4876
|
start: span.start,
|
|
4047
4877
|
end: span.end,
|
|
4048
4878
|
allDay: span.allDay,
|
|
4049
|
-
|
|
4879
|
+
// The zone the times were spelled in, kept so every surface can print
|
|
4880
|
+
// both clocks — and so a rule walks that zone's own wall calendar.
|
|
4881
|
+
tzid: zone,
|
|
4050
4882
|
location: input.location ?? "",
|
|
4051
4883
|
url: input.url ?? "",
|
|
4052
4884
|
notes: input.notes ?? "",
|
|
4053
|
-
...buildRecurrence(input.recurrence),
|
|
4885
|
+
...buildRecurrence(input.recurrence, zone),
|
|
4054
4886
|
seriesId: "",
|
|
4055
4887
|
seriesKey: "",
|
|
4056
4888
|
color: input.color ?? "",
|
|
4057
4889
|
icon: input.icon ?? "",
|
|
4058
|
-
task: span.task,
|
|
4059
|
-
doneKeys: [],
|
|
4060
4890
|
// Busy. There is no tool argument for this yet, and an agent asked to put
|
|
4061
4891
|
// an hour in the diary means the hour to be spoken for.
|
|
4062
4892
|
free: false,
|
|
4063
|
-
// Inheriting
|
|
4064
|
-
//
|
|
4065
|
-
//
|
|
4066
|
-
remindLead: REMIND_INHERIT,
|
|
4893
|
+
// Inheriting unless the caller named a lead of its own — an event created
|
|
4894
|
+
// with one it was not asked for would be an agent quietly deciding how
|
|
4895
|
+
// much warning its user wants.
|
|
4896
|
+
remindLead: input.remindLead !== void 0 ? leadFrom(input.remindLead) : REMIND_INHERIT,
|
|
4067
4897
|
// Nobody, for the reason there is no tool argument and never will be: an
|
|
4068
4898
|
// agent has no channel to invite anyone on either, so a guest list it
|
|
4069
4899
|
// assembled would be a list of people who were never asked.
|
|
4070
4900
|
organizer: null,
|
|
4071
4901
|
attendees: [],
|
|
4072
|
-
attendeeCount: 0
|
|
4902
|
+
attendeeCount: 0,
|
|
4903
|
+
// The stamp is the caller's: `performCreateEvent` and the quick-add path
|
|
4904
|
+
// are where this record starts existing, the same rule `draftEvent`
|
|
4905
|
+
// follows in the app.
|
|
4906
|
+
updatedAt: 0
|
|
4073
4907
|
});
|
|
4074
4908
|
}
|
|
4075
4909
|
function applyPatchToEvent(base, input, calendars, settings, anchorMs = base.start) {
|
|
4076
4910
|
const calendar = input.calendarId !== void 0 ? resolveCalendar(input.calendarId, calendars, settings) : calendars.find((c) => c.id === base.calendarId) ?? resolveCalendar(void 0, calendars, settings);
|
|
4077
|
-
const
|
|
4911
|
+
const zone = input.zone !== void 0 ? zoneFrom(input.zone) : base.tzid;
|
|
4912
|
+
const span = resolveSpanForPatch(input, base, anchorMs, zone);
|
|
4078
4913
|
return validate({
|
|
4079
4914
|
title: input.title ?? base.title,
|
|
4080
4915
|
calendarId: calendar.id,
|
|
4081
4916
|
start: span.start,
|
|
4082
4917
|
end: span.end,
|
|
4083
4918
|
allDay: span.allDay,
|
|
4084
|
-
|
|
4919
|
+
// The patch's zone, else the base's — cleared with the clock when the
|
|
4920
|
+
// patch flips the event all-day, since an all-day event is never zoned.
|
|
4921
|
+
tzid: span.allDay ? "" : zone,
|
|
4085
4922
|
location: input.location ?? base.location,
|
|
4086
4923
|
url: input.url ?? base.url,
|
|
4087
4924
|
notes: input.notes ?? base.notes,
|
|
4088
|
-
...input.recurrence ? buildRecurrence(input.recurrence) : recurrenceOf(base),
|
|
4925
|
+
...input.recurrence ? buildRecurrence(input.recurrence, zone) : recurrenceOf(base),
|
|
4089
4926
|
seriesId: base.seriesId,
|
|
4090
4927
|
seriesKey: base.seriesKey,
|
|
4091
4928
|
color: input.color ?? base.color,
|
|
4092
4929
|
icon: input.icon ?? base.icon,
|
|
4093
|
-
task: span.task,
|
|
4094
|
-
doneKeys: base.doneKeys,
|
|
4095
4930
|
// Carried, like every other field a patch does not name: `validate` reads
|
|
4096
4931
|
// the draft through `backup.readEvent`, which defaults an absent `free`
|
|
4097
4932
|
// to busy — so leaving it out would quietly rebook a free event the
|
|
4098
4933
|
// moment its title was edited.
|
|
4099
4934
|
free: base.free,
|
|
4100
|
-
// Carried
|
|
4101
|
-
//
|
|
4102
|
-
// moment an assistant fixed a typo in its title
|
|
4103
|
-
|
|
4104
|
-
remindLead: base.remindLead,
|
|
4935
|
+
// Carried unless the patch names one — `readEvent` reads an absent lead
|
|
4936
|
+
// as inheriting, so leaving it out would un-silence an event the user
|
|
4937
|
+
// silenced the moment an assistant fixed a typo in its title.
|
|
4938
|
+
remindLead: input.remindLead !== void 0 ? leadFrom(input.remindLead) : base.remindLead,
|
|
4105
4939
|
// Carried, and this is the field where dropping it would be least
|
|
4106
4940
|
// recoverable: the guest list is a record of a message somebody else sent,
|
|
4107
4941
|
// and `readEvent` reads an absent one as no invitation at all — so an
|
|
@@ -4109,25 +4943,73 @@ function applyPatchToEvent(base, input, calendars, settings, anchorMs = base.sta
|
|
|
4109
4943
|
// had asked and who was coming.
|
|
4110
4944
|
organizer: base.organizer,
|
|
4111
4945
|
attendees: base.attendees,
|
|
4112
|
-
attendeeCount: base.attendeeCount
|
|
4946
|
+
attendeeCount: base.attendeeCount,
|
|
4947
|
+
// Carried, and overwritten by the reducer that commits the patch: it is
|
|
4948
|
+
// the base's stamp until this edit lands, never a fresh record's zero.
|
|
4949
|
+
updatedAt: base.updatedAt
|
|
4113
4950
|
});
|
|
4114
4951
|
}
|
|
4952
|
+
function carried(contents) {
|
|
4953
|
+
return {
|
|
4954
|
+
events: contents.events,
|
|
4955
|
+
calendars: contents.calendars,
|
|
4956
|
+
tasks: contents.tasks,
|
|
4957
|
+
settings: contents.settings,
|
|
4958
|
+
settingsAt: contents.settingsAt,
|
|
4959
|
+
graves: contents.graves
|
|
4960
|
+
};
|
|
4961
|
+
}
|
|
4962
|
+
var ID_SHAPE_RE = /^([0-9a-z]+-[0-9a-z]{6}|x-.+)$/;
|
|
4963
|
+
function notFound(contents, id, kind) {
|
|
4964
|
+
const grave = contents.graves.find((g) => g.id === id);
|
|
4965
|
+
if (grave) {
|
|
4966
|
+
return new RefusalError(
|
|
4967
|
+
`no ${kind} with id "${id}" \u2014 deleted ${new Date(grave.at).toISOString()} (a grave for it is in the document, so this writer or another one removed it)`
|
|
4968
|
+
);
|
|
4969
|
+
}
|
|
4970
|
+
if (ID_SHAPE_RE.test(id)) {
|
|
4971
|
+
return new RefusalError(
|
|
4972
|
+
`no ${kind} with id "${id}", and no grave for it either \u2014 the id has this app's shape, so another writer may have dropped or rewritten it; re-read with list_day or search`
|
|
4973
|
+
);
|
|
4974
|
+
}
|
|
4975
|
+
return new RefusalError(`no ${kind} with id "${id}"`);
|
|
4976
|
+
}
|
|
4115
4977
|
function calendarName(calendars, id) {
|
|
4116
4978
|
return calendars.find((c) => c.id === id)?.name ?? "\u2014";
|
|
4117
4979
|
}
|
|
4980
|
+
function zoneSuffix(event, clock) {
|
|
4981
|
+
if (!event.tzid || event.allDay) return "";
|
|
4982
|
+
const start = formatTimeShort(wallClockIn(event.start, event.tzid), clock);
|
|
4983
|
+
const end = formatTimeShort(wallClockIn(event.end, event.tzid), clock);
|
|
4984
|
+
return ` (${start}\u2013${end} ${event.tzid})`;
|
|
4985
|
+
}
|
|
4118
4986
|
function eventSummaryLine(event, calendars, clock) {
|
|
4119
|
-
const when = event.
|
|
4987
|
+
const when = event.allDay ? `${dayKey(new Date(event.start))} \u2013 ${dayKey(addDays(new Date(event.end), -1))} (all day)` : `${dayKey(new Date(event.start))} ${formatTimeShort(new Date(event.start), clock)}\u2013${formatTimeShort(new Date(event.end), clock)}${zoneSuffix(event, clock)}`;
|
|
4120
4988
|
return `${event.title || "Untitled"} \u2014 [${calendarName(calendars, event.calendarId)}] \u2014 ${when} \u2014 id=${event.id}`;
|
|
4121
4989
|
}
|
|
4990
|
+
function taskSummaryLine(task, calendars, clock) {
|
|
4991
|
+
const when = task.dueAllDay ? `due ${dayKey(new Date(task.due))}` : `due ${dayKey(new Date(task.due))} ${formatTimeShort(new Date(task.due), clock)}`;
|
|
4992
|
+
const bits = [
|
|
4993
|
+
`${task.title || "Untitled"} (task)`,
|
|
4994
|
+
`[${calendarName(calendars, task.calendarId)}]`,
|
|
4995
|
+
when,
|
|
4996
|
+
task.done ? "done" : "open",
|
|
4997
|
+
...task.estimate !== null ? [`~${formatDuration(task.estimate)}`] : [],
|
|
4998
|
+
...task.priority > 0 ? [task.priority === 2 ? "urgent" : "high"] : [],
|
|
4999
|
+
`id=${task.id}`
|
|
5000
|
+
];
|
|
5001
|
+
return bits.join(" \u2014 ");
|
|
5002
|
+
}
|
|
4122
5003
|
function occurrenceLine(occ, calendars, clock) {
|
|
4123
|
-
const
|
|
5004
|
+
const pip = occ.task && occ.start.getTime() === occ.end.getTime();
|
|
5005
|
+
const when = occ.event.allDay ? "all day" : pip ? `${occ.start.toISOString()} (${formatTimeShort(occ.start, clock)}) due` : `${occ.start.toISOString()} (${formatTimeShort(occ.start, clock)}) \u2013 ${occ.end.toISOString()} (${formatTimeShort(occ.end, clock)})`;
|
|
4124
5006
|
const span = spanLabel(occ);
|
|
4125
5007
|
const bits = [
|
|
4126
5008
|
occ.event.title || "Untitled",
|
|
4127
5009
|
`[${calendarName(calendars, occ.event.calendarId)}]`,
|
|
4128
5010
|
when,
|
|
4129
5011
|
...span ? [span] : [],
|
|
4130
|
-
...occ.
|
|
5012
|
+
...occ.task ? [occ.done ? "done" : "open"] : [],
|
|
4131
5013
|
`id=${occ.event.id}`,
|
|
4132
5014
|
`occurrence=${dayKey(occ.start)}`
|
|
4133
5015
|
];
|
|
@@ -4136,7 +5018,7 @@ function occurrenceLine(occ, calendars, clock) {
|
|
|
4136
5018
|
function listDay(contents, date) {
|
|
4137
5019
|
assertDateKey(date);
|
|
4138
5020
|
const day = fromDayKey(date);
|
|
4139
|
-
const agenda = dayAgenda(contents.events, contents.calendars, day);
|
|
5021
|
+
const agenda = dayAgenda(contents.events, contents.calendars, day, contents.tasks);
|
|
4140
5022
|
const clock = contents.settings.clock;
|
|
4141
5023
|
return [
|
|
4142
5024
|
`${date} \u2014 ${agenda.all.length} occurrence${agenda.all.length === 1 ? "" : "s"}`,
|
|
@@ -4166,7 +5048,10 @@ function listRange(contents, from, to) {
|
|
|
4166
5048
|
let truncated = false;
|
|
4167
5049
|
for (let i = 0; i < span && !truncated; i++) {
|
|
4168
5050
|
const day = addDays(fromDay, i);
|
|
4169
|
-
const occs =
|
|
5051
|
+
const occs = [
|
|
5052
|
+
...occurrencesOn(contents.events, contents.calendars, day),
|
|
5053
|
+
...taskOccurrences(contents.tasks, contents.calendars, day)
|
|
5054
|
+
].sort((a, b) => a.start.getTime() - b.start.getTime() || a.end.getTime() - b.end.getTime());
|
|
4170
5055
|
if (occs.length === 0) continue;
|
|
4171
5056
|
lines.push(`${dayKey(day)}:`);
|
|
4172
5057
|
for (const occ of occs) {
|
|
@@ -4183,16 +5068,29 @@ function listRange(contents, from, to) {
|
|
|
4183
5068
|
}
|
|
4184
5069
|
function searchEvents(contents, query, limit = 20) {
|
|
4185
5070
|
const clock = contents.settings.clock;
|
|
4186
|
-
const
|
|
5071
|
+
const cap = Math.max(0, limit);
|
|
5072
|
+
const scored = contents.events.map((event) => ({
|
|
5073
|
+
line: eventSummaryLine(event, contents.calendars, clock),
|
|
5074
|
+
hit: scoreEventBy(event, query)
|
|
5075
|
+
})).concat(
|
|
5076
|
+
contents.tasks.map((task) => ({
|
|
5077
|
+
line: taskSummaryLine(task, contents.calendars, clock),
|
|
5078
|
+
hit: scoreTaskBy(task, query)
|
|
5079
|
+
}))
|
|
5080
|
+
).filter((s) => s.hit !== null).sort((a, b) => b.hit.score - a.hit.score).slice(0, cap);
|
|
4187
5081
|
return [
|
|
4188
5082
|
`"${query}" \u2014 ${scored.length} match${scored.length === 1 ? "" : "es"}`,
|
|
4189
|
-
|
|
5083
|
+
// A hit outside the title says so: a notes match on a long note is
|
|
5084
|
+
// otherwise indistinguishable from what the record is called.
|
|
5085
|
+
...scored.length ? scored.map(
|
|
5086
|
+
({ line, hit }) => hit.field === "title" ? line : `${line} \u2014 matched ${hit.field}`
|
|
5087
|
+
) : ["(none)"]
|
|
4190
5088
|
].join("\n");
|
|
4191
5089
|
}
|
|
4192
5090
|
function freeSlots(contents, date, minutes, from, to) {
|
|
4193
5091
|
assertDateKey(date);
|
|
4194
5092
|
const day = fromDayKey(date);
|
|
4195
|
-
const occs =
|
|
5093
|
+
const occs = dayAgenda(contents.events, contents.calendars, day, contents.tasks).all;
|
|
4196
5094
|
const fromMin = from !== void 0 ? parseTime(from, "from") : contents.settings.wakingFrom ?? DEFAULT_WAKING.from;
|
|
4197
5095
|
const toMin = to !== void 0 ? parseTime(to, "to") : contents.settings.wakingTo ?? DEFAULT_WAKING.to;
|
|
4198
5096
|
const clock = contents.settings.clock;
|
|
@@ -4213,24 +5111,56 @@ function listCalendars(contents) {
|
|
|
4213
5111
|
}
|
|
4214
5112
|
function describeEvent(contents, id) {
|
|
4215
5113
|
const event = contents.events.find((e) => e.id === id);
|
|
4216
|
-
if (!event)
|
|
5114
|
+
if (!event) {
|
|
5115
|
+
const task = contents.tasks.find((t) => t.id === id);
|
|
5116
|
+
if (task) return describeTask(contents, task);
|
|
5117
|
+
throw notFound(contents, id, "event");
|
|
5118
|
+
}
|
|
4217
5119
|
const calendar = contents.calendars.find((c) => c.id === event.calendarId);
|
|
4218
5120
|
const clock = contents.settings.clock;
|
|
4219
5121
|
return [
|
|
4220
5122
|
`${event.title || "Untitled"} (id=${event.id})`,
|
|
4221
5123
|
`Calendar: ${calendarName(contents.calendars, event.calendarId)}${calendar?.feed ? " (subscription, read-only)" : ""}`,
|
|
4222
|
-
event.
|
|
5124
|
+
event.allDay ? `All day, ${dayKey(new Date(event.start))} to ${dayKey(addDays(new Date(event.end), -1))}` : `${dayKey(new Date(event.start))} ${formatTimeShort(new Date(event.start), clock)}\u2013${formatTimeShort(new Date(event.end), clock)}`,
|
|
5125
|
+
// Both clocks for a zoned event — the times above are local, and the
|
|
5126
|
+
// hours the deadline was written in are the ones its owner reasons in.
|
|
5127
|
+
...event.tzid && !event.allDay ? [
|
|
5128
|
+
`Zone: ${event.tzid} \u2014 ${formatTimeShort(wallClockIn(event.start, event.tzid), clock)}\u2013${formatTimeShort(wallClockIn(event.end, event.tzid), clock)} there`
|
|
5129
|
+
] : [],
|
|
4223
5130
|
`Repeats: ${repeats(event) ? describeRule(event) : "never"}`,
|
|
5131
|
+
...event.remindLead !== REMIND_INHERIT ? [`Remind: ${event.remindLead === REMIND_SILENT ? "silent" : remindLabel(event.remindLead)}`] : [],
|
|
4224
5132
|
...event.location ? [`Location: ${event.location}`] : [],
|
|
4225
5133
|
...event.url ? [`URL: ${event.url}`] : [],
|
|
4226
5134
|
...event.notes ? [`Notes: ${event.notes}`] : [],
|
|
4227
|
-
...event.seriesId ? [`Overrides one occurrence of id=${event.seriesId} (${event.seriesKey})`] : []
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
5135
|
+
...event.seriesId ? [`Overrides one occurrence of id=${event.seriesId} (${event.seriesKey})`] : []
|
|
5136
|
+
].join("\n");
|
|
5137
|
+
}
|
|
5138
|
+
function describeTask(contents, task) {
|
|
5139
|
+
const clock = contents.settings.clock;
|
|
5140
|
+
return [
|
|
5141
|
+
`${task.title || "Untitled"} (task, id=${task.id})`,
|
|
5142
|
+
`Calendar: ${calendarName(contents.calendars, task.calendarId)}`,
|
|
5143
|
+
task.dueAllDay ? `Due ${dayKey(new Date(task.due))}` : `Due ${dayKey(new Date(task.due))} ${formatTimeShort(new Date(task.due), clock)}`,
|
|
5144
|
+
`Status: ${task.done ? "done" : "open"}`,
|
|
5145
|
+
...task.estimate !== null ? [`Estimate: ${formatDuration(task.estimate)}`] : [],
|
|
5146
|
+
...task.priority > 0 ? [`Priority: ${task.priority === 2 ? "urgent" : "high"}`] : [],
|
|
5147
|
+
...task.plans.length ? task.plans.map(
|
|
5148
|
+
(p) => `Planned: ${dayKey(new Date(p.start))} ${formatTimeShort(new Date(p.start), clock)}\u2013${formatTimeShort(new Date(p.end), clock)}`
|
|
5149
|
+
) : [],
|
|
5150
|
+
...task.notes ? [`Notes: ${task.notes}`] : []
|
|
5151
|
+
].join("\n");
|
|
5152
|
+
}
|
|
5153
|
+
function syncStatus(contents, transport) {
|
|
5154
|
+
return [
|
|
5155
|
+
`Document: rev ${contents.rev}, format version ${contents.version}, zone ${contents.zone || "(unrecorded)"}`,
|
|
5156
|
+
`Transport: ${transport}`,
|
|
5157
|
+
`Counts: ${contents.events.length} events, ${contents.tasks.length} tasks, ${contents.calendars.length} calendars, ${contents.graves.length} graves`
|
|
4231
5158
|
].join("\n");
|
|
4232
5159
|
}
|
|
4233
|
-
function
|
|
5160
|
+
function defaultedSuffix(defaulted) {
|
|
5161
|
+
return defaulted ? " \u2014 calendar defaulted, not named" : "";
|
|
5162
|
+
}
|
|
5163
|
+
function performQuickAdd(contents, text, date, pin) {
|
|
4234
5164
|
if (!text.trim()) throw new RefusalError('"text" is empty');
|
|
4235
5165
|
const reference = date ? parseDay(date) : startOfDay(/* @__PURE__ */ new Date());
|
|
4236
5166
|
const parsed = parseQuickAdd(text, reference, /* @__PURE__ */ new Date(), contents.settings.defaultDuration);
|
|
@@ -4239,46 +5169,200 @@ function performQuickAdd(contents, text, date) {
|
|
|
4239
5169
|
`could not read "${text}" as an event \u2014 name something beyond a bare title, e.g. "lunch tomorrow 1pm 45m" or "gym every monday 7am"`
|
|
4240
5170
|
);
|
|
4241
5171
|
}
|
|
4242
|
-
const calendar = resolveCalendar(void 0, contents.calendars, contents.settings);
|
|
5172
|
+
const calendar = resolveCalendar(void 0, contents.calendars, contents.settings, pin);
|
|
4243
5173
|
const record = validate({
|
|
4244
5174
|
...draftEvent(parsed.day, parsed.startMin, parsed.endMin, calendar.id),
|
|
4245
5175
|
title: parsed.title,
|
|
4246
5176
|
...parsed.repeat
|
|
4247
5177
|
});
|
|
4248
|
-
const event = { ...record, id: newId() };
|
|
5178
|
+
const event = { ...record, id: newId(), updatedAt: Date.now() };
|
|
4249
5179
|
return {
|
|
4250
|
-
mutated: {
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
5180
|
+
mutated: { ...carried(contents), events: [...contents.events, event] },
|
|
5181
|
+
message: `Created ${eventSummaryLine(event, contents.calendars, contents.settings.clock)}` + defaultedSuffix(defaultedCalendar(void 0, pin))
|
|
5182
|
+
};
|
|
5183
|
+
}
|
|
5184
|
+
var KEYED_ID_PREFIX = "x-";
|
|
5185
|
+
function keyedId(key) {
|
|
5186
|
+
return `${KEYED_ID_PREFIX}${key}`;
|
|
5187
|
+
}
|
|
5188
|
+
function performCreateEvent(contents, input, key, pin) {
|
|
5189
|
+
const suffix = defaultedSuffix(defaultedCalendar(input.calendarId, pin));
|
|
5190
|
+
if (key) {
|
|
5191
|
+
const id = keyedId(key);
|
|
5192
|
+
const existing = contents.events.find((e) => e.id === id);
|
|
5193
|
+
if (existing) {
|
|
5194
|
+
refuseIfFeed(existing, contents.calendars);
|
|
5195
|
+
const record3 = applyPatchToEvent(existing, input, contents.calendars, contents.settings);
|
|
5196
|
+
const events = applyUpdate(contents.events, id, record3, Date.now());
|
|
5197
|
+
const updated = events.find((e) => e.id === id);
|
|
5198
|
+
return {
|
|
5199
|
+
mutated: { ...carried(contents), events },
|
|
5200
|
+
message: `Updated (by key "${key}") ` + eventSummaryLine(updated, contents.calendars, contents.settings.clock) + suffix
|
|
5201
|
+
};
|
|
5202
|
+
}
|
|
5203
|
+
const record2 = buildEventRecord(input, contents.calendars, contents.settings, pin);
|
|
5204
|
+
const event2 = { ...record2, id, updatedAt: Date.now() };
|
|
5205
|
+
return {
|
|
5206
|
+
mutated: { ...carried(contents), events: [...contents.events, event2] },
|
|
5207
|
+
message: `Created ${eventSummaryLine(event2, contents.calendars, contents.settings.clock)}` + suffix
|
|
5208
|
+
};
|
|
5209
|
+
}
|
|
5210
|
+
const record = buildEventRecord(input, contents.calendars, contents.settings, pin);
|
|
5211
|
+
const event = { ...record, id: newId(), updatedAt: Date.now() };
|
|
5212
|
+
return {
|
|
5213
|
+
mutated: { ...carried(contents), events: [...contents.events, event] },
|
|
5214
|
+
message: `Created ${eventSummaryLine(event, contents.calendars, contents.settings.clock)}` + suffix
|
|
5215
|
+
};
|
|
5216
|
+
}
|
|
5217
|
+
var CREATE_BATCH_MAX = 100;
|
|
5218
|
+
function performCreateEvents(contents, inputs, pin) {
|
|
5219
|
+
if (inputs.length === 0) throw new RefusalError('"events" is empty');
|
|
5220
|
+
if (inputs.length > CREATE_BATCH_MAX) {
|
|
5221
|
+
throw new RefusalError(
|
|
5222
|
+
`the batch is capped at ${CREATE_BATCH_MAX} events \u2014 got ${inputs.length}`
|
|
5223
|
+
);
|
|
5224
|
+
}
|
|
5225
|
+
let current = contents;
|
|
5226
|
+
const lines = [];
|
|
5227
|
+
for (const [index, input] of inputs.entries()) {
|
|
5228
|
+
let outcome;
|
|
5229
|
+
try {
|
|
5230
|
+
outcome = performCreateEvent(current, input, input.key, pin);
|
|
5231
|
+
} catch (error) {
|
|
5232
|
+
if (error instanceof RefusalError) {
|
|
5233
|
+
throw new RefusalError(`event ${index + 1} of ${inputs.length}: ${error.message}`);
|
|
5234
|
+
}
|
|
5235
|
+
throw error;
|
|
5236
|
+
}
|
|
5237
|
+
current = { ...current, ...outcome.mutated };
|
|
5238
|
+
lines.push(outcome.message);
|
|
5239
|
+
}
|
|
5240
|
+
return { mutated: carried(current), message: lines.join("\n") };
|
|
5241
|
+
}
|
|
5242
|
+
var PRIORITY_RANK = { normal: 0, high: 1, urgent: 2 };
|
|
5243
|
+
function requireTask(contents, id, tool) {
|
|
5244
|
+
const task = contents.tasks.find((t) => t.id === id);
|
|
5245
|
+
if (task) return task;
|
|
5246
|
+
if (contents.events.some((e) => e.id === id)) {
|
|
5247
|
+
throw new RefusalError(`"${id}" is an event, not a task \u2014 ${tool} works on tasks`);
|
|
5248
|
+
}
|
|
5249
|
+
throw notFound(contents, id, "task");
|
|
5250
|
+
}
|
|
5251
|
+
function requireValidTask(assembled) {
|
|
5252
|
+
const task = readTask(assembled);
|
|
5253
|
+
if (!task) throw new RefusalError("the assembled task failed validation (check date and time)");
|
|
5254
|
+
return task;
|
|
5255
|
+
}
|
|
5256
|
+
function performCreateTask(contents, input, pin) {
|
|
5257
|
+
if (!input.date) throw new RefusalError('a task needs "date" (YYYY-MM-DD), its due day');
|
|
5258
|
+
const day = parseDay(input.date);
|
|
5259
|
+
const calendar = resolveCalendar(input.calendarId, contents.calendars, contents.settings, pin);
|
|
5260
|
+
const dueAllDay = input.time === void 0 || input.time === null;
|
|
5261
|
+
const task = requireValidTask({
|
|
5262
|
+
id: newId(),
|
|
5263
|
+
title: input.title ?? "",
|
|
5264
|
+
notes: input.notes ?? "",
|
|
5265
|
+
calendarId: calendar.id,
|
|
5266
|
+
icon: input.icon ?? "",
|
|
5267
|
+
color: input.color ?? "",
|
|
5268
|
+
due: dueAllDay ? startOfDay(day).getTime() : atMinutes(day, parseTime(input.time, "time")).getTime(),
|
|
5269
|
+
dueAllDay,
|
|
5270
|
+
estimate: input.estimate ?? null,
|
|
5271
|
+
priority: PRIORITY_RANK[input.priority ?? "normal"],
|
|
5272
|
+
plans: [],
|
|
5273
|
+
done: false,
|
|
5274
|
+
updatedAt: Date.now()
|
|
5275
|
+
});
|
|
5276
|
+
return {
|
|
5277
|
+
mutated: { ...carried(contents), tasks: [...contents.tasks, task] },
|
|
5278
|
+
message: `Created ${taskSummaryLine(task, contents.calendars, contents.settings.clock)}` + defaultedSuffix(defaultedCalendar(input.calendarId, pin))
|
|
4256
5279
|
};
|
|
4257
5280
|
}
|
|
4258
|
-
function
|
|
4259
|
-
const
|
|
4260
|
-
const
|
|
5281
|
+
function performUpdateTask(contents, id, patch) {
|
|
5282
|
+
const original = requireTask(contents, id, "update_task");
|
|
5283
|
+
const day = patch.date ? parseDay(patch.date) : startOfDay(new Date(original.due));
|
|
5284
|
+
const dueAllDay = patch.time === null ? true : patch.time !== void 0 ? false : original.dueAllDay;
|
|
5285
|
+
const dueMin = typeof patch.time === "string" ? parseTime(patch.time, "time") : minutesOfDay(new Date(original.due));
|
|
5286
|
+
const calendar = patch.calendarId ? resolveCalendar(patch.calendarId, contents.calendars, contents.settings) : void 0;
|
|
5287
|
+
const next = requireValidTask({
|
|
5288
|
+
...original,
|
|
5289
|
+
title: patch.title ?? original.title,
|
|
5290
|
+
notes: patch.notes ?? original.notes,
|
|
5291
|
+
calendarId: calendar?.id ?? original.calendarId,
|
|
5292
|
+
color: patch.color ?? original.color,
|
|
5293
|
+
icon: patch.icon ?? original.icon,
|
|
5294
|
+
due: dueAllDay ? startOfDay(day).getTime() : atMinutes(day, dueMin).getTime(),
|
|
5295
|
+
dueAllDay,
|
|
5296
|
+
estimate: patch.estimate !== void 0 ? patch.estimate : original.estimate,
|
|
5297
|
+
priority: patch.priority !== void 0 ? PRIORITY_RANK[patch.priority] : original.priority,
|
|
5298
|
+
updatedAt: Date.now()
|
|
5299
|
+
});
|
|
4261
5300
|
return {
|
|
4262
5301
|
mutated: {
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
settings: contents.settings
|
|
5302
|
+
...carried(contents),
|
|
5303
|
+
tasks: contents.tasks.map((t) => t.id === id ? next : t)
|
|
4266
5304
|
},
|
|
4267
|
-
message: `
|
|
5305
|
+
message: `Updated ${taskSummaryLine(next, contents.calendars, contents.settings.clock)}`
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5308
|
+
function performPlanTask(contents, id, date, start, end, duration) {
|
|
5309
|
+
const original = requireTask(contents, id, "plan_task");
|
|
5310
|
+
if (original.done) throw new RefusalError("this task is already done \u2014 nothing to plan");
|
|
5311
|
+
if (original.plans.length >= PLANS_MAX) {
|
|
5312
|
+
throw new RefusalError(`this task already carries ${PLANS_MAX} plans, the cap`);
|
|
5313
|
+
}
|
|
5314
|
+
const day = parseDay(date);
|
|
5315
|
+
const startMin = parseTime(start, "start");
|
|
5316
|
+
const endMin = end !== void 0 ? parseTime(end, "end") : startMin + (duration ?? original.estimate ?? contents.settings.defaultDuration);
|
|
5317
|
+
if (endMin <= startMin) throw new RefusalError('"end" must be after "start"');
|
|
5318
|
+
const plan = { start: atMinutes(day, startMin).getTime(), end: atMinutes(day, endMin).getTime() };
|
|
5319
|
+
const next = {
|
|
5320
|
+
...original,
|
|
5321
|
+
plans: [...original.plans, plan].sort((a, b) => a.start - b.start || a.end - b.end),
|
|
5322
|
+
updatedAt: Date.now()
|
|
5323
|
+
};
|
|
5324
|
+
const clock = contents.settings.clock;
|
|
5325
|
+
return {
|
|
5326
|
+
mutated: { ...carried(contents), tasks: contents.tasks.map((t) => t.id === id ? next : t) },
|
|
5327
|
+
message: `Planned "${next.title || "Untitled"}" ${date} ${formatTimeShort(new Date(plan.start), clock)}\u2013${formatTimeShort(new Date(plan.end), clock)}`
|
|
4268
5328
|
};
|
|
4269
5329
|
}
|
|
5330
|
+
function listTasks(contents, now = /* @__PURE__ */ new Date()) {
|
|
5331
|
+
if (contents.tasks.length === 0) return "No tasks.";
|
|
5332
|
+
const clock = contents.settings.clock;
|
|
5333
|
+
const groups = [
|
|
5334
|
+
["Overdue", []],
|
|
5335
|
+
["Due soon", []],
|
|
5336
|
+
["Later", []],
|
|
5337
|
+
["Done", []]
|
|
5338
|
+
];
|
|
5339
|
+
const index = { overdue: 0, "due-soon": 1, later: 2, done: 3 };
|
|
5340
|
+
for (const task of contents.tasks) groups[index[taskState(task, now)]][1].push(task);
|
|
5341
|
+
const sections = groups.filter(([, rows]) => rows.length > 0).map(
|
|
5342
|
+
([label, rows]) => [
|
|
5343
|
+
`${label} (${rows.length}):`,
|
|
5344
|
+
...rows.sort(compareTasks).map((t) => ` ${taskSummaryLine(t, contents.calendars, clock)}`)
|
|
5345
|
+
].join("\n")
|
|
5346
|
+
);
|
|
5347
|
+
return sections.join("\n\n");
|
|
5348
|
+
}
|
|
4270
5349
|
function performUpdateEvent(contents, id, scopeArg, occurrence, patch) {
|
|
4271
5350
|
const original = contents.events.find((e) => e.id === id);
|
|
4272
|
-
if (!original)
|
|
5351
|
+
if (!original) {
|
|
5352
|
+
if (contents.tasks.some((t) => t.id === id)) {
|
|
5353
|
+
throw new RefusalError(`id "${id}" is a task \u2014 use update_task, plan_task or toggle_done.`);
|
|
5354
|
+
}
|
|
5355
|
+
throw notFound(contents, id, "event");
|
|
5356
|
+
}
|
|
4273
5357
|
refuseIfFeed(original, contents.calendars);
|
|
4274
5358
|
const scope = requireScope(original, scopeArg);
|
|
4275
5359
|
const clock = contents.settings.clock;
|
|
4276
5360
|
if (scope === "all") {
|
|
4277
5361
|
const record2 = applyPatchToEvent(original, patch, contents.calendars, contents.settings);
|
|
4278
|
-
const events = applyUpdate(contents.events, original.id, record2);
|
|
5362
|
+
const events = applyUpdate(contents.events, original.id, record2, Date.now());
|
|
4279
5363
|
const updated = events.find((e) => e.id === original.id);
|
|
4280
5364
|
return {
|
|
4281
|
-
mutated: {
|
|
5365
|
+
mutated: { ...carried(contents), events },
|
|
4282
5366
|
message: `Updated ${eventSummaryLine(updated, contents.calendars, clock)}`
|
|
4283
5367
|
};
|
|
4284
5368
|
}
|
|
@@ -4290,71 +5374,102 @@ function performUpdateEvent(contents, id, scopeArg, occurrence, patch) {
|
|
|
4290
5374
|
...NEVER,
|
|
4291
5375
|
seriesId: original.id,
|
|
4292
5376
|
seriesKey: instantKey(at),
|
|
4293
|
-
id: newId()
|
|
5377
|
+
id: newId(),
|
|
5378
|
+
// An agent's write is a mutation like the app's, so it stamps like one:
|
|
5379
|
+
// unstamped, the browser's own copy of this document wins on the next
|
|
5380
|
+
// merge and the agent's work is quietly dropped.
|
|
5381
|
+
updatedAt: Date.now()
|
|
4294
5382
|
};
|
|
4295
5383
|
return {
|
|
4296
|
-
mutated: {
|
|
4297
|
-
events: [...contents.events, override],
|
|
4298
|
-
calendars: contents.calendars,
|
|
4299
|
-
settings: contents.settings
|
|
4300
|
-
},
|
|
5384
|
+
mutated: { ...carried(contents), events: [...contents.events, override] },
|
|
4301
5385
|
message: `Updated one occurrence: ${eventSummaryLine(override, contents.calendars, clock)}`
|
|
4302
5386
|
};
|
|
4303
5387
|
}
|
|
4304
5388
|
const record = applyPatchToEvent(original, patch, contents.calendars, contents.settings, at);
|
|
4305
|
-
const split = applySplit(contents.events, original.id, at, record, newId);
|
|
5389
|
+
const split = applySplit(contents.events, original.id, at, record, newId, Date.now());
|
|
4306
5390
|
if (!split.created) throw new RefusalError(`no event with id "${id}"`);
|
|
4307
5391
|
return {
|
|
4308
|
-
mutated: { events: split.events
|
|
5392
|
+
mutated: { ...carried(contents), events: split.events },
|
|
4309
5393
|
message: `Updated from ${dayKey(new Date(at))} on: ${eventSummaryLine(split.created, contents.calendars, clock)}`
|
|
4310
5394
|
};
|
|
4311
5395
|
}
|
|
4312
5396
|
function performDeleteEvent(contents, id, scopeArg, occurrence) {
|
|
4313
5397
|
const original = contents.events.find((e) => e.id === id);
|
|
4314
|
-
if (!original)
|
|
5398
|
+
if (!original) {
|
|
5399
|
+
if (contents.tasks.some((t) => t.id === id)) return performDeleteTask(contents, id);
|
|
5400
|
+
throw notFound(contents, id, "event");
|
|
5401
|
+
}
|
|
4315
5402
|
refuseIfFeed(original, contents.calendars);
|
|
4316
5403
|
const scope = requireScope(original, scopeArg);
|
|
4317
5404
|
const title = original.title || "Untitled";
|
|
4318
5405
|
if (scope === "all") {
|
|
5406
|
+
const removed = applyRemove(contents.events, original.id, Date.now());
|
|
4319
5407
|
return {
|
|
5408
|
+
// The graves ride with the removal, because an agent's deletion has to
|
|
5409
|
+
// survive the browser's next merge: without them the tab still holding
|
|
5410
|
+
// the event wins on stamps and writes it straight back.
|
|
4320
5411
|
mutated: {
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
5412
|
+
...carried(contents),
|
|
5413
|
+
events: removed.events,
|
|
5414
|
+
graves: [...contents.graves, ...removed.graves]
|
|
4324
5415
|
},
|
|
4325
5416
|
message: `Deleted "${title}" (the whole series).`
|
|
4326
5417
|
};
|
|
4327
5418
|
}
|
|
4328
5419
|
const at = resolveOccurrenceRequired(original, occurrence, scope);
|
|
4329
5420
|
if (scope === "one") {
|
|
4330
|
-
const events = applyUpdate(
|
|
4331
|
-
|
|
4332
|
-
|
|
5421
|
+
const events = applyUpdate(
|
|
5422
|
+
contents.events,
|
|
5423
|
+
original.id,
|
|
5424
|
+
{ exdates: [...original.exdates, instantKey(at)] },
|
|
5425
|
+
Date.now()
|
|
5426
|
+
);
|
|
4333
5427
|
return {
|
|
4334
|
-
mutated: {
|
|
5428
|
+
mutated: { ...carried(contents), events },
|
|
4335
5429
|
message: `Deleted "${title}" on ${dayKey(new Date(at))} only \u2014 the series continues.`
|
|
4336
5430
|
};
|
|
4337
5431
|
}
|
|
5432
|
+
const cut = applyTruncate(contents.events, original.id, at, Date.now());
|
|
4338
5433
|
return {
|
|
4339
5434
|
mutated: {
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
5435
|
+
...carried(contents),
|
|
5436
|
+
events: cut.events,
|
|
5437
|
+
graves: [...contents.graves, ...cut.graves]
|
|
4343
5438
|
},
|
|
4344
5439
|
message: `Deleted "${title}" from ${dayKey(new Date(at))} on.`
|
|
4345
5440
|
};
|
|
4346
5441
|
}
|
|
4347
|
-
function
|
|
4348
|
-
const
|
|
4349
|
-
if (!
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
5442
|
+
function performDeleteTask(contents, id) {
|
|
5443
|
+
const task = contents.tasks.find((t) => t.id === id);
|
|
5444
|
+
if (!task) {
|
|
5445
|
+
if (contents.events.some((e) => e.id === id)) {
|
|
5446
|
+
throw new RefusalError(`"${id}" is an event, not a task \u2014 use delete_event`);
|
|
5447
|
+
}
|
|
5448
|
+
throw notFound(contents, id, "task");
|
|
5449
|
+
}
|
|
5450
|
+
return {
|
|
5451
|
+
mutated: {
|
|
5452
|
+
...carried(contents),
|
|
5453
|
+
tasks: contents.tasks.filter((t) => t.id !== id),
|
|
5454
|
+
graves: [...contents.graves, { id, at: Date.now() }]
|
|
5455
|
+
},
|
|
5456
|
+
message: `Deleted the task "${task.title || "Untitled"}".`
|
|
5457
|
+
};
|
|
5458
|
+
}
|
|
5459
|
+
function performToggleDone(contents, id, _occurrence) {
|
|
5460
|
+
const original = contents.tasks.find((t) => t.id === id);
|
|
5461
|
+
if (!original) {
|
|
5462
|
+
if (contents.events.some((e) => e.id === id)) {
|
|
5463
|
+
throw new RefusalError(`"${id}" is an event, not a task`);
|
|
5464
|
+
}
|
|
5465
|
+
throw notFound(contents, id, "task");
|
|
5466
|
+
}
|
|
5467
|
+
const tasks = contents.tasks.map(
|
|
5468
|
+
(t) => t.id === id ? { ...t, done: !t.done, updatedAt: Date.now() } : t
|
|
5469
|
+
);
|
|
4355
5470
|
return {
|
|
4356
|
-
mutated: {
|
|
4357
|
-
message: `${
|
|
5471
|
+
mutated: { ...carried(contents), tasks },
|
|
5472
|
+
message: `${original.done ? "Reopened" : "Completed"} "${original.title || "Untitled"}".`
|
|
4358
5473
|
};
|
|
4359
5474
|
}
|
|
4360
5475
|
function performCreateCalendar(contents, name, color, icon) {
|
|
@@ -4365,15 +5480,12 @@ function performCreateCalendar(contents, name, color, icon) {
|
|
|
4365
5480
|
name: trimmed,
|
|
4366
5481
|
color: color || PALETTE[contents.calendars.length * 3 % PALETTE.length],
|
|
4367
5482
|
icon: icon ?? "",
|
|
4368
|
-
visible: true
|
|
5483
|
+
visible: true,
|
|
5484
|
+
updatedAt: Date.now()
|
|
4369
5485
|
});
|
|
4370
5486
|
if (!calendar) throw new RefusalError("the assembled calendar failed validation");
|
|
4371
5487
|
return {
|
|
4372
|
-
mutated: {
|
|
4373
|
-
events: contents.events,
|
|
4374
|
-
calendars: [...contents.calendars, calendar],
|
|
4375
|
-
settings: contents.settings
|
|
4376
|
-
},
|
|
5488
|
+
mutated: { ...carried(contents), calendars: [...contents.calendars, calendar] },
|
|
4377
5489
|
message: `Created calendar "${calendar.name}" \u2014 id=${calendar.id}`
|
|
4378
5490
|
};
|
|
4379
5491
|
}
|
|
@@ -4434,13 +5546,12 @@ function eventFieldsShape() {
|
|
|
4434
5546
|
return {
|
|
4435
5547
|
title: z.string().optional(),
|
|
4436
5548
|
date: dateKey("YYYY-MM-DD, local.").optional(),
|
|
4437
|
-
start: z.string().describe("HH:MM, 24-hour local clock. Required for a timed event
|
|
5549
|
+
start: z.string().describe("HH:MM, 24-hour local clock. Required for a timed event.").optional(),
|
|
4438
5550
|
end: z.string().describe(
|
|
4439
5551
|
"HH:MM for a timed event, or YYYY-MM-DD (inclusive) for a multi-day all-day event. Alternative to duration."
|
|
4440
5552
|
).optional(),
|
|
4441
5553
|
duration: z.number().describe("Minutes \u2014 an alternative to end, for a timed event.").optional(),
|
|
4442
5554
|
allDay: z.boolean().describe("Forces an all-day event. Inferred when start, end and duration are all omitted.").optional(),
|
|
4443
|
-
task: z.boolean().describe("A dated to-do (one moment, not a span) rather than an event.").optional(),
|
|
4444
5555
|
calendarId: z.string().describe(
|
|
4445
5556
|
"Defaults to the user's default calendar. Must not be a subscription \u2014 those are read-only."
|
|
4446
5557
|
).optional(),
|
|
@@ -4449,29 +5560,65 @@ function eventFieldsShape() {
|
|
|
4449
5560
|
location: z.string().optional(),
|
|
4450
5561
|
url: z.string().optional(),
|
|
4451
5562
|
notes: z.string().optional(),
|
|
5563
|
+
zone: z.string().describe(
|
|
5564
|
+
`IANA zone the date and times are written in, e.g. "Asia/Seoul" \u2014 for a deadline abroad. Stored on the event so every surface prints both clocks; timed events only. On update, "" clears it back to floating (local) time, and patched times are read on the event's own zone.`
|
|
5565
|
+
).optional(),
|
|
5566
|
+
remindLead: z.union([z.number().int().min(0).max(REMIND_LEAD_MAX), z.enum(["inherit", "silent"])]).describe(
|
|
5567
|
+
'Reminder lead: minutes before the start (0 = as it starts), "silent" (never fires), or "inherit" (follow the calendar, then the setting \u2014 the default).'
|
|
5568
|
+
).optional(),
|
|
4452
5569
|
recurrence: recurrenceSchema.optional()
|
|
4453
5570
|
};
|
|
4454
5571
|
}
|
|
5572
|
+
var upsertKeySchema = z.string().regex(/^[A-Za-z0-9._:-]{1,64}$/, "letters, digits, . _ : -, up to 64").describe(
|
|
5573
|
+
"Caller-chosen external key for idempotent upsert: the same key always names the same event, so re-running a mirror updates in place instead of duplicating. Omitted, every call creates a fresh event."
|
|
5574
|
+
);
|
|
4455
5575
|
var createEventFieldsSchema = z.strictObject({
|
|
4456
5576
|
...eventFieldsShape(),
|
|
4457
|
-
date: dateKey("YYYY-MM-DD, local.")
|
|
5577
|
+
date: dateKey("YYYY-MM-DD, local."),
|
|
5578
|
+
key: upsertKeySchema.optional()
|
|
4458
5579
|
});
|
|
4459
5580
|
var patchEventFieldsSchema = z.strictObject(eventFieldsShape());
|
|
5581
|
+
function taskFieldsShape() {
|
|
5582
|
+
return {
|
|
5583
|
+
title: z.string().optional(),
|
|
5584
|
+
date: dateKey("YYYY-MM-DD, local \u2014 the due day.").optional(),
|
|
5585
|
+
time: z.string().nullable().describe(
|
|
5586
|
+
"HH:MM due time, 24-hour local clock. Omit for a deadline that is a date with no time on it; null (on update) clears an existing due time back to that."
|
|
5587
|
+
).optional(),
|
|
5588
|
+
estimate: z.number().nullable().describe("Minutes of work this looks like. null (on update) clears it back to unknown.").optional(),
|
|
5589
|
+
priority: z.enum(["normal", "high", "urgent"]).optional(),
|
|
5590
|
+
calendarId: z.string().describe(
|
|
5591
|
+
"Defaults to the user's default calendar. Must not be a subscription \u2014 those are read-only."
|
|
5592
|
+
).optional(),
|
|
5593
|
+
color: z.string().describe("#rrggbb. Empty follows the calendar's own colour.").optional(),
|
|
5594
|
+
icon: z.string().describe("A Phosphor icon name or one emoji. Empty follows the calendar's own mark.").optional(),
|
|
5595
|
+
notes: z.string().optional()
|
|
5596
|
+
};
|
|
5597
|
+
}
|
|
5598
|
+
var createTaskFieldsSchema = z.strictObject({
|
|
5599
|
+
...taskFieldsShape(),
|
|
5600
|
+
date: dateKey("YYYY-MM-DD, local \u2014 the due day.")
|
|
5601
|
+
});
|
|
5602
|
+
var patchTaskFieldsSchema = z.strictObject(taskFieldsShape());
|
|
4460
5603
|
var SCOPE_DESCRIPTION = 'Required when the target event repeats: "one" (just this occurrence), "future" (this one and everything after), or "all" (the whole series). Ignored when the event does not repeat.';
|
|
4461
5604
|
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
5605
|
var scopeSchema = z.enum(["one", "future", "all"]).describe(SCOPE_DESCRIPTION);
|
|
4463
5606
|
var occurrenceSchema = z.string().describe(OCCURRENCE_DESCRIPTION);
|
|
4464
|
-
|
|
5607
|
+
var calendarFilterSchema = z.string().describe("Scope to one calendar \u2014 its id, or a unique name prefix. Omit for every calendar.");
|
|
5608
|
+
function registerTools(server, store3, pin) {
|
|
4465
5609
|
server.registerTool(
|
|
4466
5610
|
"list_day",
|
|
4467
5611
|
{
|
|
4468
5612
|
description: "The agenda for one local day: recurrence expanded, all-day and timed occurrences listed separately, each with its id (for update_event/delete_event/toggle_done), its occurrence key, and \u2014 for a multi-day span \u2014 its Day N/M position.",
|
|
4469
|
-
inputSchema: z.strictObject({
|
|
5613
|
+
inputSchema: z.strictObject({
|
|
5614
|
+
date: dateKey("YYYY-MM-DD, local."),
|
|
5615
|
+
calendar: calendarFilterSchema.optional()
|
|
5616
|
+
}),
|
|
4470
5617
|
annotations: { readOnlyHint: true }
|
|
4471
5618
|
},
|
|
4472
|
-
async ({ date }) => guarded(async () => {
|
|
4473
|
-
const { contents, warning } = await
|
|
4474
|
-
return withWarning(listDay(contents, date), warning);
|
|
5619
|
+
async ({ date, calendar }) => guarded(async () => {
|
|
5620
|
+
const { contents, warning } = await store3.load();
|
|
5621
|
+
return withWarning(listDay(scopedToCalendar(contents, calendar), date), warning);
|
|
4475
5622
|
})
|
|
4476
5623
|
);
|
|
4477
5624
|
server.registerTool(
|
|
@@ -4480,28 +5627,33 @@ function registerTools(server, ctx2) {
|
|
|
4480
5627
|
description: `The agenda across several local days, one section per day that has something on it. Capped at ${MAX_RANGE_DAYS} days of span and ${MAX_RANGE_OCCURRENCES} occurrences total \u2014 a wide range may come back truncated, which the response says.`,
|
|
4481
5628
|
inputSchema: z.strictObject({
|
|
4482
5629
|
from: dateKey("YYYY-MM-DD, local, inclusive."),
|
|
4483
|
-
to: dateKey("YYYY-MM-DD, local, inclusive.")
|
|
5630
|
+
to: dateKey("YYYY-MM-DD, local, inclusive."),
|
|
5631
|
+
calendar: calendarFilterSchema.optional()
|
|
4484
5632
|
}),
|
|
4485
5633
|
annotations: { readOnlyHint: true }
|
|
4486
5634
|
},
|
|
4487
|
-
async ({ from, to }) => guarded(async () => {
|
|
4488
|
-
const { contents, warning } = await
|
|
4489
|
-
return withWarning(listRange(contents, from, to), warning);
|
|
5635
|
+
async ({ from, to, calendar }) => guarded(async () => {
|
|
5636
|
+
const { contents, warning } = await store3.load();
|
|
5637
|
+
return withWarning(listRange(scopedToCalendar(contents, calendar), from, to), warning);
|
|
4490
5638
|
})
|
|
4491
5639
|
);
|
|
4492
5640
|
server.registerTool(
|
|
4493
5641
|
"search",
|
|
4494
5642
|
{
|
|
4495
|
-
description: "Ranked title/location/notes search over the user's own events (never a subscription's).",
|
|
5643
|
+
description: "Ranked title/location/notes search over the user's own events (never a subscription's). Each result names the field it matched when it was not the title.",
|
|
4496
5644
|
inputSchema: z.strictObject({
|
|
4497
5645
|
query: z.string(),
|
|
4498
|
-
limit: z.number().int().min(1).describe("Default 20.").optional()
|
|
5646
|
+
limit: z.number().int().min(1).describe("Default 20.").optional(),
|
|
5647
|
+
calendar: calendarFilterSchema.optional()
|
|
4499
5648
|
}),
|
|
4500
5649
|
annotations: { readOnlyHint: true }
|
|
4501
5650
|
},
|
|
4502
|
-
async ({ query, limit }) => guarded(async () => {
|
|
4503
|
-
const { contents, warning } = await
|
|
4504
|
-
return withWarning(
|
|
5651
|
+
async ({ query, limit, calendar }) => guarded(async () => {
|
|
5652
|
+
const { contents, warning } = await store3.load();
|
|
5653
|
+
return withWarning(
|
|
5654
|
+
searchEvents(scopedToCalendar(contents, calendar), query, limit),
|
|
5655
|
+
warning
|
|
5656
|
+
);
|
|
4505
5657
|
})
|
|
4506
5658
|
);
|
|
4507
5659
|
server.registerTool(
|
|
@@ -4512,13 +5664,17 @@ function registerTools(server, ctx2) {
|
|
|
4512
5664
|
date: dateKey("YYYY-MM-DD, local."),
|
|
4513
5665
|
minutes: z.number().describe("Minimum slot length, in minutes."),
|
|
4514
5666
|
from: z.string().describe("HH:MM, local. Defaults to the user's waking-hours start.").optional(),
|
|
4515
|
-
to: z.string().describe("HH:MM, local. Defaults to the user's waking-hours end.").optional()
|
|
5667
|
+
to: z.string().describe("HH:MM, local. Defaults to the user's waking-hours end.").optional(),
|
|
5668
|
+
calendar: calendarFilterSchema.optional()
|
|
4516
5669
|
}),
|
|
4517
5670
|
annotations: { readOnlyHint: true }
|
|
4518
5671
|
},
|
|
4519
|
-
async ({ date, minutes, from, to }) => guarded(async () => {
|
|
4520
|
-
const { contents, warning } = await
|
|
4521
|
-
return withWarning(
|
|
5672
|
+
async ({ date, minutes, from, to, calendar }) => guarded(async () => {
|
|
5673
|
+
const { contents, warning } = await store3.load();
|
|
5674
|
+
return withWarning(
|
|
5675
|
+
freeSlots(scopedToCalendar(contents, calendar), date, minutes, from, to),
|
|
5676
|
+
warning
|
|
5677
|
+
);
|
|
4522
5678
|
})
|
|
4523
5679
|
);
|
|
4524
5680
|
server.registerTool(
|
|
@@ -4529,7 +5685,7 @@ function registerTools(server, ctx2) {
|
|
|
4529
5685
|
annotations: { readOnlyHint: true }
|
|
4530
5686
|
},
|
|
4531
5687
|
async () => guarded(async () => {
|
|
4532
|
-
const { contents, warning } = await
|
|
5688
|
+
const { contents, warning } = await store3.load();
|
|
4533
5689
|
return withWarning(listCalendars(contents), warning);
|
|
4534
5690
|
})
|
|
4535
5691
|
);
|
|
@@ -4541,7 +5697,7 @@ function registerTools(server, ctx2) {
|
|
|
4541
5697
|
annotations: { readOnlyHint: true }
|
|
4542
5698
|
},
|
|
4543
5699
|
async ({ id }) => guarded(async () => {
|
|
4544
|
-
const { contents, warning } = await
|
|
5700
|
+
const { contents, warning } = await store3.load();
|
|
4545
5701
|
return withWarning(describeEvent(contents, id), warning);
|
|
4546
5702
|
})
|
|
4547
5703
|
);
|
|
@@ -4558,8 +5714,8 @@ function registerTools(server, ctx2) {
|
|
|
4558
5714
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
4559
5715
|
},
|
|
4560
5716
|
async ({ text, date }) => guarded(
|
|
4561
|
-
() =>
|
|
4562
|
-
const outcome = performQuickAdd(contents, text, date);
|
|
5717
|
+
() => store3.write((contents) => {
|
|
5718
|
+
const outcome = performQuickAdd(contents, text, date, pin);
|
|
4563
5719
|
return { result: outcome.mutated, summary: outcome.message };
|
|
4564
5720
|
})
|
|
4565
5721
|
)
|
|
@@ -4567,13 +5723,30 @@ function registerTools(server, ctx2) {
|
|
|
4567
5723
|
server.registerTool(
|
|
4568
5724
|
"create_event",
|
|
4569
5725
|
{
|
|
4570
|
-
description: "Creates an event from explicit fields, for anything quick_add's grammar cannot spell \u2014 a full recurrence, a specific calendar, notes. Every field is validated the same way an imported backup is.",
|
|
5726
|
+
description: "Creates an event from explicit fields, for anything quick_add's grammar cannot spell \u2014 a full recurrence, a specific calendar, notes, a foreign time zone, a reminder lead. Every field is validated the same way an imported backup is. `key` makes the call an idempotent upsert.",
|
|
4571
5727
|
inputSchema: createEventFieldsSchema,
|
|
4572
5728
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
4573
5729
|
},
|
|
4574
5730
|
async (input) => guarded(
|
|
4575
|
-
() =>
|
|
4576
|
-
const
|
|
5731
|
+
() => store3.write((contents) => {
|
|
5732
|
+
const { key, ...fields } = input;
|
|
5733
|
+
const outcome = performCreateEvent(contents, fields, key, pin);
|
|
5734
|
+
return { result: outcome.mutated, summary: outcome.message };
|
|
5735
|
+
})
|
|
5736
|
+
)
|
|
5737
|
+
);
|
|
5738
|
+
server.registerTool(
|
|
5739
|
+
"create_events",
|
|
5740
|
+
{
|
|
5741
|
+
description: `Creates (or, with a per-record \`key\`, upserts) up to ${CREATE_BATCH_MAX} events in one write \u2014 one revision bump, all-or-nothing: any refused record refuses the whole batch naming which. The convergent way to mirror an external document into the calendar.`,
|
|
5742
|
+
inputSchema: z.strictObject({
|
|
5743
|
+
events: z.array(createEventFieldsSchema).min(1).max(CREATE_BATCH_MAX)
|
|
5744
|
+
}),
|
|
5745
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
5746
|
+
},
|
|
5747
|
+
async ({ events }) => guarded(
|
|
5748
|
+
() => store3.write((contents) => {
|
|
5749
|
+
const outcome = performCreateEvents(contents, events, pin);
|
|
4577
5750
|
return { result: outcome.mutated, summary: outcome.message };
|
|
4578
5751
|
})
|
|
4579
5752
|
)
|
|
@@ -4591,7 +5764,7 @@ function registerTools(server, ctx2) {
|
|
|
4591
5764
|
annotations: { destructiveHint: true, idempotentHint: false }
|
|
4592
5765
|
},
|
|
4593
5766
|
async ({ id, scope, occurrence, patch }) => guarded(
|
|
4594
|
-
() =>
|
|
5767
|
+
() => store3.write((contents) => {
|
|
4595
5768
|
const outcome = performUpdateEvent(contents, id, scope, occurrence, patch);
|
|
4596
5769
|
return { result: outcome.mutated, summary: outcome.message };
|
|
4597
5770
|
})
|
|
@@ -4609,29 +5782,103 @@ function registerTools(server, ctx2) {
|
|
|
4609
5782
|
annotations: { destructiveHint: true, idempotentHint: false }
|
|
4610
5783
|
},
|
|
4611
5784
|
async ({ id, scope, occurrence }) => guarded(
|
|
4612
|
-
() =>
|
|
5785
|
+
() => store3.write((contents) => {
|
|
4613
5786
|
const outcome = performDeleteEvent(contents, id, scope, occurrence);
|
|
4614
5787
|
return { result: outcome.mutated, summary: outcome.message };
|
|
4615
5788
|
})
|
|
4616
5789
|
)
|
|
4617
5790
|
);
|
|
5791
|
+
server.registerTool(
|
|
5792
|
+
"delete_task",
|
|
5793
|
+
{
|
|
5794
|
+
description: "Deletes a task. The same removal delete_event performs on a task id \u2014 this name exists so the task verbs read as the set they are (create_task, update_task, delete_task).",
|
|
5795
|
+
inputSchema: z.strictObject({ id: z.string() }),
|
|
5796
|
+
annotations: { destructiveHint: true, idempotentHint: false }
|
|
5797
|
+
},
|
|
5798
|
+
async ({ id }) => guarded(
|
|
5799
|
+
() => store3.write((contents) => {
|
|
5800
|
+
const outcome = performDeleteTask(contents, id);
|
|
5801
|
+
return { result: outcome.mutated, summary: outcome.message };
|
|
5802
|
+
})
|
|
5803
|
+
)
|
|
5804
|
+
);
|
|
4618
5805
|
server.registerTool(
|
|
4619
5806
|
"toggle_done",
|
|
4620
5807
|
{
|
|
4621
|
-
description: "Ticks a task off, or back on
|
|
5808
|
+
description: "Ticks a task off, or back on \u2014 a task has one deadline and is finished once.",
|
|
4622
5809
|
inputSchema: z.strictObject({
|
|
4623
5810
|
id: z.string(),
|
|
4624
|
-
occurrence: z.string().describe("
|
|
5811
|
+
occurrence: z.string().describe("Accepted for compatibility and ignored \u2014 a task is ticked whole.").optional()
|
|
4625
5812
|
}),
|
|
4626
5813
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
4627
5814
|
},
|
|
4628
5815
|
async ({ id, occurrence }) => guarded(
|
|
4629
|
-
() =>
|
|
5816
|
+
() => store3.write((contents) => {
|
|
4630
5817
|
const outcome = performToggleDone(contents, id, occurrence);
|
|
4631
5818
|
return { result: outcome.mutated, summary: outcome.message };
|
|
4632
5819
|
})
|
|
4633
5820
|
)
|
|
4634
5821
|
);
|
|
5822
|
+
server.registerTool(
|
|
5823
|
+
"list_tasks",
|
|
5824
|
+
{
|
|
5825
|
+
description: "Every task, grouped the way the app's own sidebar groups them \u2014 Overdue, Due soon, Later, Done \u2014 soonest deadline first inside each group.",
|
|
5826
|
+
inputSchema: z.strictObject({}),
|
|
5827
|
+
annotations: { readOnlyHint: true }
|
|
5828
|
+
},
|
|
5829
|
+
async () => guarded(async () => {
|
|
5830
|
+
const { contents, warning } = await store3.load();
|
|
5831
|
+
return withWarning(listTasks(contents), warning);
|
|
5832
|
+
})
|
|
5833
|
+
);
|
|
5834
|
+
server.registerTool(
|
|
5835
|
+
"create_task",
|
|
5836
|
+
{
|
|
5837
|
+
description: "Creates a task: a piece of work with a deadline, distinct from an event. `date` is the due day; add `time` for a timed deadline, `estimate` so auto-schedule can size it.",
|
|
5838
|
+
inputSchema: createTaskFieldsSchema,
|
|
5839
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
5840
|
+
},
|
|
5841
|
+
async (input) => guarded(
|
|
5842
|
+
() => store3.write((contents) => {
|
|
5843
|
+
const outcome = performCreateTask(contents, input, pin);
|
|
5844
|
+
return { result: outcome.mutated, summary: outcome.message };
|
|
5845
|
+
})
|
|
5846
|
+
)
|
|
5847
|
+
);
|
|
5848
|
+
server.registerTool(
|
|
5849
|
+
"update_task",
|
|
5850
|
+
{
|
|
5851
|
+
description: "Patches an existing task. Every field in `patch` is optional and left alone when omitted; ticking is toggle_done's job and planning is plan_task's.",
|
|
5852
|
+
inputSchema: z.strictObject({ id: z.string(), patch: patchTaskFieldsSchema }),
|
|
5853
|
+
annotations: { destructiveHint: true, idempotentHint: false }
|
|
5854
|
+
},
|
|
5855
|
+
async ({ id, patch }) => guarded(
|
|
5856
|
+
() => store3.write((contents) => {
|
|
5857
|
+
const outcome = performUpdateTask(contents, id, patch);
|
|
5858
|
+
return { result: outcome.mutated, summary: outcome.message };
|
|
5859
|
+
})
|
|
5860
|
+
)
|
|
5861
|
+
);
|
|
5862
|
+
server.registerTool(
|
|
5863
|
+
"plan_task",
|
|
5864
|
+
{
|
|
5865
|
+
description: "Books a block of time for a task on the clock \u2014 the drag onto the dial, performed by the assistant. Length falls back to the task's estimate, then the default duration.",
|
|
5866
|
+
inputSchema: z.strictObject({
|
|
5867
|
+
id: z.string(),
|
|
5868
|
+
date: dateKey("YYYY-MM-DD, local \u2014 the day to work on it."),
|
|
5869
|
+
start: z.string().describe("HH:MM, 24-hour local clock."),
|
|
5870
|
+
end: z.string().describe("HH:MM. Alternative to duration.").optional(),
|
|
5871
|
+
duration: z.number().describe("Minutes \u2014 an alternative to end.").optional()
|
|
5872
|
+
}),
|
|
5873
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
5874
|
+
},
|
|
5875
|
+
async ({ id, date, start, end, duration }) => guarded(
|
|
5876
|
+
() => store3.write((contents) => {
|
|
5877
|
+
const outcome = performPlanTask(contents, id, date, start, end, duration);
|
|
5878
|
+
return { result: outcome.mutated, summary: outcome.message };
|
|
5879
|
+
})
|
|
5880
|
+
)
|
|
5881
|
+
);
|
|
4635
5882
|
server.registerTool(
|
|
4636
5883
|
"create_calendar",
|
|
4637
5884
|
{
|
|
@@ -4644,19 +5891,43 @@ function registerTools(server, ctx2) {
|
|
|
4644
5891
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
4645
5892
|
},
|
|
4646
5893
|
async ({ name, color, icon }) => guarded(
|
|
4647
|
-
() =>
|
|
5894
|
+
() => store3.write((contents) => {
|
|
4648
5895
|
const outcome = performCreateCalendar(contents, name, color, icon);
|
|
4649
5896
|
return { result: outcome.mutated, summary: outcome.message };
|
|
4650
5897
|
})
|
|
4651
5898
|
)
|
|
4652
5899
|
);
|
|
5900
|
+
server.registerTool(
|
|
5901
|
+
"sync_status",
|
|
5902
|
+
{
|
|
5903
|
+
description: "The document's envelope and shape \u2014 revision, format version, recorded zone, transport, and record counts (events, tasks, calendars, graves). The first tool to reach for when reasoning about sync, a lost record, or a revision that moved.",
|
|
5904
|
+
inputSchema: z.strictObject({}),
|
|
5905
|
+
annotations: { readOnlyHint: true }
|
|
5906
|
+
},
|
|
5907
|
+
async () => guarded(async () => {
|
|
5908
|
+
const { contents, warning } = await store3.load();
|
|
5909
|
+
return withWarning(syncStatus(contents, store3.transport), warning);
|
|
5910
|
+
})
|
|
5911
|
+
);
|
|
4653
5912
|
}
|
|
4654
5913
|
|
|
4655
5914
|
// mcp/index.ts
|
|
4656
|
-
var HELP = `circal-mcp \u2014 reads and writes circal
|
|
5915
|
+
var HELP = `circal-mcp \u2014 reads and writes a circal calendar over stdio (MCP)
|
|
4657
5916
|
|
|
4658
5917
|
Usage:
|
|
4659
5918
|
circal-mcp --file <path> Start the server against a mirror file
|
|
5919
|
+
circal-mcp --cloud Start the server against the cloud copy
|
|
5920
|
+
(sign in first with \`circal-mcp login\`)
|
|
5921
|
+
circal-mcp --calendar <c> Pin this connection's default calendar (an id or
|
|
5922
|
+
unique name prefix): writes that name none land
|
|
5923
|
+
on it instead of the user default. Optional,
|
|
5924
|
+
beside either transport
|
|
5925
|
+
circal-mcp login Sign in to the cloud copy and remember the
|
|
5926
|
+
derived key on this machine (interactive:
|
|
5927
|
+
email, emailed code, passphrase; asks circal.day
|
|
5928
|
+
which project it uses \u2014 --site <url> for a fork,
|
|
5929
|
+
or --url and --key to name one outright)
|
|
5930
|
+
circal-mcp logout Forget the remembered key and session
|
|
4660
5931
|
circal-mcp --find Search common locations for a mirror file and
|
|
4661
5932
|
print a ready-to-paste client config (read-only,
|
|
4662
5933
|
does not start the server)
|
|
@@ -4664,8 +5935,11 @@ Usage:
|
|
|
4664
5935
|
|
|
4665
5936
|
Environment:
|
|
4666
5937
|
CIRCAL_FILE Mirror file path, same as --file
|
|
5938
|
+
CIRCAL_CALENDAR Default calendar for writes, same as --calendar
|
|
5939
|
+
CIRCAL_CLOUD_CREDENTIALS Where \`login\` keeps its record (default:
|
|
5940
|
+
the platform config dir, circal/cloud.json)
|
|
4667
5941
|
CIRCAL_ALLOW_ZONE_MISMATCH Set to 1 to allow writes when this process's
|
|
4668
|
-
zone does not match the
|
|
5942
|
+
zone does not match the document's recorded zone
|
|
4669
5943
|
`;
|
|
4670
5944
|
var argv = process.argv.slice(2);
|
|
4671
5945
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
@@ -4676,18 +5950,43 @@ if (argv.includes("--find")) {
|
|
|
4676
5950
|
await runFind();
|
|
4677
5951
|
process.exit(0);
|
|
4678
5952
|
}
|
|
4679
|
-
|
|
4680
|
-
try {
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
5953
|
+
if (argv[0] === "login" || argv[0] === "logout") {
|
|
5954
|
+
try {
|
|
5955
|
+
if (argv[0] === "login") await runLogin(argv, process.env);
|
|
5956
|
+
else await runLogout(process.env);
|
|
5957
|
+
process.exit(0);
|
|
5958
|
+
} catch (error) {
|
|
5959
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
5960
|
+
process.exit(1);
|
|
5961
|
+
}
|
|
5962
|
+
}
|
|
5963
|
+
var store2;
|
|
5964
|
+
if (argv.includes("--cloud")) {
|
|
5965
|
+
const path = credentialsPath(process.env);
|
|
5966
|
+
const credentials = await readCredentials(path);
|
|
5967
|
+
if (!credentials) {
|
|
5968
|
+
console.error(
|
|
5969
|
+
`circal-mcp --cloud needs a sign-in and found none at ${path}. Run \`circal-mcp login\` first.`
|
|
5970
|
+
);
|
|
5971
|
+
process.exit(1);
|
|
5972
|
+
}
|
|
5973
|
+
store2 = cloudStore({ credentials, path, env: process.env });
|
|
5974
|
+
} else {
|
|
5975
|
+
let path;
|
|
5976
|
+
try {
|
|
5977
|
+
path = resolveFilePath(argv, process.env);
|
|
5978
|
+
} catch (error) {
|
|
5979
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5980
|
+
console.error(`${message} Run \`circal-mcp --find\` to search common locations for one.`);
|
|
5981
|
+
process.exit(1);
|
|
5982
|
+
}
|
|
5983
|
+
store2 = fileStore({ path, env: process.env });
|
|
4686
5984
|
}
|
|
4687
|
-
var
|
|
5985
|
+
var calendarFlag = argv.indexOf("--calendar");
|
|
5986
|
+
var pinned = (calendarFlag >= 0 ? argv[calendarFlag + 1] : void 0) || process.env.CIRCAL_CALENDAR;
|
|
4688
5987
|
function createServer() {
|
|
4689
5988
|
const server = new McpServer({ name: "circal", version: "1.0.0" });
|
|
4690
|
-
registerTools(server,
|
|
5989
|
+
registerTools(server, store2, pinned);
|
|
4691
5990
|
return server;
|
|
4692
5991
|
}
|
|
4693
5992
|
void serveStdio(createServer);
|