circal-mcp 0.3.0 → 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 +10 -0
- package/dist/circal-mcp.mjs +774 -153
- package/package.json +1 -1
package/dist/circal-mcp.mjs
CHANGED
|
@@ -2332,10 +2332,9 @@ function occurrencesOn(events, calendars, day) {
|
|
|
2332
2332
|
continuesAfter,
|
|
2333
2333
|
dayIndex: daysBetween(firstDay, dayStart) + 1,
|
|
2334
2334
|
dayCount: daysBetween(firstDay, lastDay) + 1,
|
|
2335
|
-
//
|
|
2336
|
-
//
|
|
2337
|
-
|
|
2338
|
-
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,
|
|
2339
2338
|
lane: 0
|
|
2340
2339
|
});
|
|
2341
2340
|
}
|
|
@@ -2347,22 +2346,99 @@ function packLanes(occurrences, pad2 = 6) {
|
|
|
2347
2346
|
const laneEnds = [];
|
|
2348
2347
|
return occurrences.map((occ) => {
|
|
2349
2348
|
if (occ.event.allDay) return occ;
|
|
2350
|
-
if (occ.
|
|
2349
|
+
if (occ.task && occ.startMin === occ.endMin) return occ;
|
|
2351
2350
|
let lane = laneEnds.findIndex((end) => end <= occ.startMin + pad2);
|
|
2352
2351
|
if (lane === -1) lane = laneEnds.length;
|
|
2353
2352
|
laneEnds[lane] = occ.endMin;
|
|
2354
2353
|
return { ...occ, lane };
|
|
2355
2354
|
});
|
|
2356
2355
|
}
|
|
2357
|
-
function dayAgenda(events, calendars, day) {
|
|
2358
|
-
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());
|
|
2359
2359
|
const allDay = all.filter((o) => o.event.allDay);
|
|
2360
2360
|
const timed = packLanes(all.filter((o) => !o.event.allDay));
|
|
2361
2361
|
return { timed, allDay, all: [...allDay, ...timed] };
|
|
2362
2362
|
}
|
|
2363
|
-
function
|
|
2364
|
-
|
|
2365
|
-
|
|
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;
|
|
2366
2442
|
}
|
|
2367
2443
|
var DEFAULT_WAKING = { from: 8 * 60, to: 22 * 60 };
|
|
2368
2444
|
function freeGaps(occurrences, minMinutes, from, to) {
|
|
@@ -2386,13 +2462,15 @@ function dayLoad(occurrences, waking) {
|
|
|
2386
2462
|
}
|
|
2387
2463
|
var FIELD_PENALTY = { location: 30, notes: 45 };
|
|
2388
2464
|
var penalise = (score, cost) => score === null ? null : score - cost;
|
|
2389
|
-
function
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
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;
|
|
2396
2474
|
}
|
|
2397
2475
|
var PALETTE = [
|
|
2398
2476
|
"#7ce3d8",
|
|
@@ -2424,15 +2502,61 @@ function spanLabel(occ) {
|
|
|
2424
2502
|
// src/lib/store.ts
|
|
2425
2503
|
var import_react2 = __toESM(require_react(), 1);
|
|
2426
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;
|
|
2526
|
+
}
|
|
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);
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2427
2556
|
// src/lib/mutate.ts
|
|
2428
2557
|
function applyUpdate(events, id, patch, now) {
|
|
2429
2558
|
return events.map((e) => e.id === id ? { ...e, ...patch, updatedAt: now } : e);
|
|
2430
2559
|
}
|
|
2431
|
-
function applyToggleDone(events, id, at, now) {
|
|
2432
|
-
return events.map(
|
|
2433
|
-
(e) => e.id === id ? { ...e, doneKeys: toggleDoneKey(e.doneKeys, at), updatedAt: now } : e
|
|
2434
|
-
);
|
|
2435
|
-
}
|
|
2436
2560
|
function applyRemove(events, id, now) {
|
|
2437
2561
|
const buried = events.filter((e) => e.id === id || e.seriesId === id);
|
|
2438
2562
|
return {
|
|
@@ -2789,6 +2913,12 @@ function parseQuickAdd(input, reference, now = /* @__PURE__ */ new Date(), fallb
|
|
|
2789
2913
|
repeat
|
|
2790
2914
|
};
|
|
2791
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
|
+
}
|
|
2792
2922
|
|
|
2793
2923
|
// src/lib/theme.ts
|
|
2794
2924
|
var THEMES = ["midnight", "ember", "paper"];
|
|
@@ -3367,7 +3497,10 @@ var DEFAULT_SETTINGS = {
|
|
|
3367
3497
|
// `DEFAULT_WAKING`'s own pair: this setting exists to replace the constant,
|
|
3368
3498
|
// not to disagree with it on day one.
|
|
3369
3499
|
wakingFrom: DEFAULT_WAKING.from,
|
|
3370
|
-
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
|
|
3371
3504
|
};
|
|
3372
3505
|
var DEFAULT_CALENDARS = [
|
|
3373
3506
|
{
|
|
@@ -3426,8 +3559,6 @@ function draftEvent(day, startMin, endMin, calendarId, repeat = "never") {
|
|
|
3426
3559
|
seriesKey: "",
|
|
3427
3560
|
color: "",
|
|
3428
3561
|
icon: "",
|
|
3429
|
-
task: false,
|
|
3430
|
-
doneKeys: [],
|
|
3431
3562
|
// Busy: a gesture that named a time meant that time to be spoken for.
|
|
3432
3563
|
free: false,
|
|
3433
3564
|
// Inheriting, which is the only answer a gesture gives: the drag named a
|
|
@@ -3565,13 +3696,12 @@ function readSettings(value) {
|
|
|
3565
3696
|
// waking window to the defaults, same as every other bounded field here.
|
|
3566
3697
|
weekStart: value.weekStart === 0 || value.weekStart === 1 || value.weekStart === 6 ? value.weekStart : DEFAULT_SETTINGS.weekStart,
|
|
3567
3698
|
wakingFrom: typeof value.wakingFrom === "number" ? value.wakingFrom : DEFAULT_SETTINGS.wakingFrom,
|
|
3568
|
-
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
|
|
3569
3703
|
};
|
|
3570
3704
|
}
|
|
3571
|
-
var INSTANT_KEY = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/;
|
|
3572
|
-
function readDoneKeys(value) {
|
|
3573
|
-
return Array.isArray(value) ? [...new Set(value.filter((k) => typeof k === "string" && INSTANT_KEY.test(k)))] : [];
|
|
3574
|
-
}
|
|
3575
3705
|
function derivedId(value, start, end) {
|
|
3576
3706
|
const text = (key) => typeof value[key] === "string" ? value[key] : "";
|
|
3577
3707
|
return `${start}-${fnv1a([start, end, text("title"), text("location"), text("seriesKey")].join("|"))}`;
|
|
@@ -3581,8 +3711,7 @@ function readEvent(value) {
|
|
|
3581
3711
|
const { id, title, calendarId, start, end } = value;
|
|
3582
3712
|
if (typeof start !== "number" || typeof end !== "number") return null;
|
|
3583
3713
|
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
3584
|
-
|
|
3585
|
-
if (task ? end < start : end <= start) return null;
|
|
3714
|
+
if (end <= start) return null;
|
|
3586
3715
|
return {
|
|
3587
3716
|
// Derived from the record's own fields, never minted: `Math.random()` gave
|
|
3588
3717
|
// a different id every read, so the same backup imported twice was two
|
|
@@ -3612,11 +3741,6 @@ function readEvent(value) {
|
|
|
3612
3741
|
// it has to tell apart. This used to slice at one shared cap, which turned
|
|
3613
3742
|
// ten of the 247 shipped names into strings matching no path.
|
|
3614
3743
|
icon: readMark(value.icon),
|
|
3615
|
-
task,
|
|
3616
|
-
// The same instant-key check EXDATE gets, and the same reason: a key this
|
|
3617
|
-
// does not spell matches no occurrence, so it is a row of dead weight that
|
|
3618
|
-
// survives every future export.
|
|
3619
|
-
doneKeys: readDoneKeys(value.doneKeys),
|
|
3620
3744
|
// Anything other than a literal `true` is busy, which is what an event
|
|
3621
3745
|
// exported before this field existed was. A truthy string in a
|
|
3622
3746
|
// hand-edited blob must not quietly take a meeting off the day's total.
|
|
@@ -3668,10 +3792,33 @@ function readCalendar(value) {
|
|
|
3668
3792
|
...feedUrl ? { feed: { url: feedUrl, fetchedAt: 0, error: "" } } : {}
|
|
3669
3793
|
};
|
|
3670
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
|
+
}
|
|
3671
3817
|
function readBackup(parsed) {
|
|
3672
3818
|
if (!isRecord(parsed) || parsed.app !== "circal") return { ok: false, reason: "not-circal" };
|
|
3673
3819
|
const events = (Array.isArray(parsed.events) ? parsed.events : []).map(readEvent).filter((e) => e !== null);
|
|
3674
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);
|
|
3675
3822
|
if (calendars.length === 0) return { ok: false, reason: "no-calendars" };
|
|
3676
3823
|
const known = new Set(calendars.map((c) => c.id));
|
|
3677
3824
|
const fallback = calendars[0].id;
|
|
@@ -3683,6 +3830,8 @@ function readBackup(parsed) {
|
|
|
3683
3830
|
return e.seriesId && !seriesIds.has(e.seriesId) ? { ...parented, seriesId: "", seriesKey: "" } : parented;
|
|
3684
3831
|
}),
|
|
3685
3832
|
calendars,
|
|
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 }),
|
|
3686
3835
|
settings: readSettings(parsed.settings),
|
|
3687
3836
|
// Not through `readSettings`: this is bookkeeping about the object rather
|
|
3688
3837
|
// than a field of it, and a reader that clamped it into `Settings` would
|
|
@@ -3693,7 +3842,7 @@ function readBackup(parsed) {
|
|
|
3693
3842
|
}
|
|
3694
3843
|
|
|
3695
3844
|
// src/lib/mirror.ts
|
|
3696
|
-
var MIRROR_VERSION =
|
|
3845
|
+
var MIRROR_VERSION = 3;
|
|
3697
3846
|
function settled(calendars) {
|
|
3698
3847
|
return calendars.map(
|
|
3699
3848
|
(c) => c.feed ? { ...c, feed: { ...c.feed, fetchedAt: 0, error: "" } } : c
|
|
@@ -3707,6 +3856,7 @@ function serialiseMirror(s, rev, zone = localZone()) {
|
|
|
3707
3856
|
zone,
|
|
3708
3857
|
rev,
|
|
3709
3858
|
events: s.events,
|
|
3859
|
+
tasks: s.tasks,
|
|
3710
3860
|
calendars: settled(s.calendars),
|
|
3711
3861
|
settings: s.settings,
|
|
3712
3862
|
settingsAt: s.settingsAt,
|
|
@@ -3727,6 +3877,7 @@ function readMirrorFile(text) {
|
|
|
3727
3877
|
const envelope = parsed;
|
|
3728
3878
|
return {
|
|
3729
3879
|
events: read.events,
|
|
3880
|
+
tasks: read.tasks,
|
|
3730
3881
|
calendars: read.calendars,
|
|
3731
3882
|
settings: read.settings,
|
|
3732
3883
|
// Both through `readBackup`, which is the boundary that already assumes
|
|
@@ -3917,6 +4068,7 @@ async function loadForRead(ctx) {
|
|
|
3917
4068
|
}
|
|
3918
4069
|
function fileStore(ctx) {
|
|
3919
4070
|
return {
|
|
4071
|
+
transport: `file ${ctx.path}`,
|
|
3920
4072
|
load: () => loadForRead(ctx),
|
|
3921
4073
|
write: (mutate) => casWrite(ctx, mutate)
|
|
3922
4074
|
};
|
|
@@ -4238,6 +4390,7 @@ function cloudStore(ctx) {
|
|
|
4238
4390
|
}
|
|
4239
4391
|
let writeQueue2 = Promise.resolve();
|
|
4240
4392
|
return {
|
|
4393
|
+
transport: `cloud (${ctx.credentials.email || "signed in"})`,
|
|
4241
4394
|
load: loadOnce,
|
|
4242
4395
|
write(mutate) {
|
|
4243
4396
|
const run = writeQueue2.then(() => writeOnce(mutate));
|
|
@@ -4571,22 +4724,47 @@ function requireScope(event, scope) {
|
|
|
4571
4724
|
}
|
|
4572
4725
|
return scope;
|
|
4573
4726
|
}
|
|
4574
|
-
function
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
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
|
+
);
|
|
4584
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));
|
|
4585
4751
|
const id = defaultCalendarId(calendars, settings.defaultCalendar);
|
|
4586
4752
|
const found = calendars.find((c) => c.id === id);
|
|
4587
4753
|
if (!found) throw new RefusalError("there is no calendar to put this on \u2014 create one first");
|
|
4588
4754
|
return found;
|
|
4589
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
|
+
}
|
|
4590
4768
|
function refuseIfFeed(event, calendars) {
|
|
4591
4769
|
const calendar = calendars.find((c) => c.id === event.calendarId);
|
|
4592
4770
|
if (calendar?.feed) {
|
|
@@ -4595,49 +4773,70 @@ function refuseIfFeed(event, calendars) {
|
|
|
4595
4773
|
);
|
|
4596
4774
|
}
|
|
4597
4775
|
}
|
|
4598
|
-
function buildRecurrence(input) {
|
|
4776
|
+
function buildRecurrence(input, zone = "") {
|
|
4599
4777
|
if (!input) return { ...NEVER };
|
|
4600
|
-
const
|
|
4778
|
+
const boundary = input.until ? addDays(fromDayKey(input.until), 1) : null;
|
|
4779
|
+
const until = boundary ? (zone ? instantOfWall(boundary, zone) : boundary.getTime()) - 1 : 0;
|
|
4601
4780
|
return readRecurrence({ ...input, until, exdates: [], rdates: [] });
|
|
4602
4781
|
}
|
|
4603
|
-
function
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
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
|
+
);
|
|
4608
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
|
+
);
|
|
4801
|
+
}
|
|
4802
|
+
return value;
|
|
4803
|
+
}
|
|
4804
|
+
function resolveSpanForCreate(input, day, defaultDuration, zone) {
|
|
4609
4805
|
const endIsDay = input.end !== void 0 && DATE_KEY_RE.test(input.end);
|
|
4610
4806
|
const noTimeGiven = input.start === void 0 && (input.end === void 0 || endIsDay) && input.duration === void 0;
|
|
4611
4807
|
const allDay = input.allDay === true || input.allDay === void 0 && noTimeGiven;
|
|
4612
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
|
+
}
|
|
4613
4814
|
const days = input.end ? Math.max(1, daysBetween(day, fromDayKey(input.end)) + 1) : 1;
|
|
4614
4815
|
return {
|
|
4615
4816
|
start: startOfDay(day).getTime(),
|
|
4616
4817
|
end: addDays(startOfDay(day), days).getTime(),
|
|
4617
|
-
allDay: true
|
|
4618
|
-
task: false
|
|
4818
|
+
allDay: true
|
|
4619
4819
|
};
|
|
4620
4820
|
}
|
|
4621
4821
|
if (input.start === void 0) throw new RefusalError('a timed event needs "start" (HH:MM)');
|
|
4622
4822
|
const startMin = parseTime(input.start, "start");
|
|
4623
4823
|
const endMin = input.end !== void 0 ? parseTime(input.end, "end") : startMin + (input.duration ?? defaultDuration);
|
|
4624
4824
|
return {
|
|
4625
|
-
start:
|
|
4626
|
-
end:
|
|
4627
|
-
allDay: false
|
|
4628
|
-
task: false
|
|
4825
|
+
start: spanInstant(day, startMin, zone),
|
|
4826
|
+
end: spanInstant(day, endMin, zone),
|
|
4827
|
+
allDay: false
|
|
4629
4828
|
};
|
|
4630
4829
|
}
|
|
4631
|
-
function resolveSpanForPatch(input, base, anchorMs) {
|
|
4632
|
-
const
|
|
4633
|
-
const
|
|
4634
|
-
if (task) {
|
|
4635
|
-
const startMin2 = input.start !== void 0 ? parseTime(input.start, "start") : minutesOfDay(new Date(anchorMs));
|
|
4636
|
-
const at = atMinutes(day, startMin2).getTime();
|
|
4637
|
-
return { start: at, end: at, allDay: false, task: true };
|
|
4638
|
-
}
|
|
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);
|
|
4639
4833
|
const allDay = input.allDay ?? base.allDay;
|
|
4640
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
|
+
}
|
|
4641
4840
|
const baseDays = Math.max(
|
|
4642
4841
|
1,
|
|
4643
4842
|
daysBetween(startOfDay(new Date(base.start)), startOfDay(new Date(base.end)))
|
|
@@ -4646,56 +4845,55 @@ function resolveSpanForPatch(input, base, anchorMs) {
|
|
|
4646
4845
|
return {
|
|
4647
4846
|
start: startOfDay(day).getTime(),
|
|
4648
4847
|
end: addDays(startOfDay(day), days).getTime(),
|
|
4649
|
-
allDay: true
|
|
4650
|
-
task: false
|
|
4848
|
+
allDay: true
|
|
4651
4849
|
};
|
|
4652
4850
|
}
|
|
4653
4851
|
const baseDurationMin = Math.max(1, Math.round((base.end - base.start) / 6e4));
|
|
4654
|
-
const startMin = input.start !== void 0 ? parseTime(input.start, "start") : minutesOfDay(
|
|
4852
|
+
const startMin = input.start !== void 0 ? parseTime(input.start, "start") : minutesOfDay(anchorWall);
|
|
4655
4853
|
const endMin = input.end !== void 0 ? parseTime(input.end, "end") : startMin + (input.duration ?? baseDurationMin);
|
|
4656
4854
|
return {
|
|
4657
|
-
start:
|
|
4658
|
-
end:
|
|
4659
|
-
allDay: false
|
|
4660
|
-
task: false
|
|
4855
|
+
start: spanInstant(day, startMin, zone),
|
|
4856
|
+
end: spanInstant(day, endMin, zone),
|
|
4857
|
+
allDay: false
|
|
4661
4858
|
};
|
|
4662
4859
|
}
|
|
4663
4860
|
function validate(draft) {
|
|
4664
4861
|
const validated = readEvent({ ...draft, id: "pending" });
|
|
4665
4862
|
if (!validated)
|
|
4666
|
-
throw new RefusalError("the assembled event failed validation (check start
|
|
4863
|
+
throw new RefusalError("the assembled event failed validation (check start and end)");
|
|
4667
4864
|
const { id: _drop, ...rest } = validated;
|
|
4668
4865
|
return rest;
|
|
4669
4866
|
}
|
|
4670
|
-
function buildEventRecord(input, calendars, settings) {
|
|
4867
|
+
function buildEventRecord(input, calendars, settings, pin) {
|
|
4671
4868
|
if (!input.date) throw new RefusalError('an event needs "date" (YYYY-MM-DD)');
|
|
4672
4869
|
const day = parseDay(input.date);
|
|
4673
|
-
const calendar = resolveCalendar(input.calendarId, calendars, settings);
|
|
4674
|
-
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);
|
|
4675
4873
|
return validate({
|
|
4676
4874
|
title: input.title ?? "",
|
|
4677
4875
|
calendarId: calendar.id,
|
|
4678
4876
|
start: span.start,
|
|
4679
4877
|
end: span.end,
|
|
4680
4878
|
allDay: span.allDay,
|
|
4681
|
-
|
|
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,
|
|
4682
4882
|
location: input.location ?? "",
|
|
4683
4883
|
url: input.url ?? "",
|
|
4684
4884
|
notes: input.notes ?? "",
|
|
4685
|
-
...buildRecurrence(input.recurrence),
|
|
4885
|
+
...buildRecurrence(input.recurrence, zone),
|
|
4686
4886
|
seriesId: "",
|
|
4687
4887
|
seriesKey: "",
|
|
4688
4888
|
color: input.color ?? "",
|
|
4689
4889
|
icon: input.icon ?? "",
|
|
4690
|
-
task: span.task,
|
|
4691
|
-
doneKeys: [],
|
|
4692
4890
|
// Busy. There is no tool argument for this yet, and an agent asked to put
|
|
4693
4891
|
// an hour in the diary means the hour to be spoken for.
|
|
4694
4892
|
free: false,
|
|
4695
|
-
// Inheriting
|
|
4696
|
-
//
|
|
4697
|
-
//
|
|
4698
|
-
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,
|
|
4699
4897
|
// Nobody, for the reason there is no tool argument and never will be: an
|
|
4700
4898
|
// agent has no channel to invite anyone on either, so a guest list it
|
|
4701
4899
|
// assembled would be a list of people who were never asked.
|
|
@@ -4710,34 +4908,34 @@ function buildEventRecord(input, calendars, settings) {
|
|
|
4710
4908
|
}
|
|
4711
4909
|
function applyPatchToEvent(base, input, calendars, settings, anchorMs = base.start) {
|
|
4712
4910
|
const calendar = input.calendarId !== void 0 ? resolveCalendar(input.calendarId, calendars, settings) : calendars.find((c) => c.id === base.calendarId) ?? resolveCalendar(void 0, calendars, settings);
|
|
4713
|
-
const
|
|
4911
|
+
const zone = input.zone !== void 0 ? zoneFrom(input.zone) : base.tzid;
|
|
4912
|
+
const span = resolveSpanForPatch(input, base, anchorMs, zone);
|
|
4714
4913
|
return validate({
|
|
4715
4914
|
title: input.title ?? base.title,
|
|
4716
4915
|
calendarId: calendar.id,
|
|
4717
4916
|
start: span.start,
|
|
4718
4917
|
end: span.end,
|
|
4719
4918
|
allDay: span.allDay,
|
|
4720
|
-
|
|
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,
|
|
4721
4922
|
location: input.location ?? base.location,
|
|
4722
4923
|
url: input.url ?? base.url,
|
|
4723
4924
|
notes: input.notes ?? base.notes,
|
|
4724
|
-
...input.recurrence ? buildRecurrence(input.recurrence) : recurrenceOf(base),
|
|
4925
|
+
...input.recurrence ? buildRecurrence(input.recurrence, zone) : recurrenceOf(base),
|
|
4725
4926
|
seriesId: base.seriesId,
|
|
4726
4927
|
seriesKey: base.seriesKey,
|
|
4727
4928
|
color: input.color ?? base.color,
|
|
4728
4929
|
icon: input.icon ?? base.icon,
|
|
4729
|
-
task: span.task,
|
|
4730
|
-
doneKeys: base.doneKeys,
|
|
4731
4930
|
// Carried, like every other field a patch does not name: `validate` reads
|
|
4732
4931
|
// the draft through `backup.readEvent`, which defaults an absent `free`
|
|
4733
4932
|
// to busy — so leaving it out would quietly rebook a free event the
|
|
4734
4933
|
// moment its title was edited.
|
|
4735
4934
|
free: base.free,
|
|
4736
|
-
// Carried
|
|
4737
|
-
//
|
|
4738
|
-
// moment an assistant fixed a typo in its title
|
|
4739
|
-
|
|
4740
|
-
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,
|
|
4741
4939
|
// Carried, and this is the field where dropping it would be least
|
|
4742
4940
|
// recoverable: the guest list is a record of a message somebody else sent,
|
|
4743
4941
|
// and `readEvent` reads an absent one as no invitation at all — so an
|
|
@@ -4755,27 +4953,63 @@ function carried(contents) {
|
|
|
4755
4953
|
return {
|
|
4756
4954
|
events: contents.events,
|
|
4757
4955
|
calendars: contents.calendars,
|
|
4956
|
+
tasks: contents.tasks,
|
|
4758
4957
|
settings: contents.settings,
|
|
4759
4958
|
settingsAt: contents.settingsAt,
|
|
4760
4959
|
graves: contents.graves
|
|
4761
4960
|
};
|
|
4762
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
|
+
}
|
|
4763
4977
|
function calendarName(calendars, id) {
|
|
4764
4978
|
return calendars.find((c) => c.id === id)?.name ?? "\u2014";
|
|
4765
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
|
+
}
|
|
4766
4986
|
function eventSummaryLine(event, calendars, clock) {
|
|
4767
|
-
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)}`;
|
|
4768
4988
|
return `${event.title || "Untitled"} \u2014 [${calendarName(calendars, event.calendarId)}] \u2014 ${when} \u2014 id=${event.id}`;
|
|
4769
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
|
+
}
|
|
4770
5003
|
function occurrenceLine(occ, calendars, clock) {
|
|
4771
|
-
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)})`;
|
|
4772
5006
|
const span = spanLabel(occ);
|
|
4773
5007
|
const bits = [
|
|
4774
5008
|
occ.event.title || "Untitled",
|
|
4775
5009
|
`[${calendarName(calendars, occ.event.calendarId)}]`,
|
|
4776
5010
|
when,
|
|
4777
5011
|
...span ? [span] : [],
|
|
4778
|
-
...occ.
|
|
5012
|
+
...occ.task ? [occ.done ? "done" : "open"] : [],
|
|
4779
5013
|
`id=${occ.event.id}`,
|
|
4780
5014
|
`occurrence=${dayKey(occ.start)}`
|
|
4781
5015
|
];
|
|
@@ -4784,7 +5018,7 @@ function occurrenceLine(occ, calendars, clock) {
|
|
|
4784
5018
|
function listDay(contents, date) {
|
|
4785
5019
|
assertDateKey(date);
|
|
4786
5020
|
const day = fromDayKey(date);
|
|
4787
|
-
const agenda = dayAgenda(contents.events, contents.calendars, day);
|
|
5021
|
+
const agenda = dayAgenda(contents.events, contents.calendars, day, contents.tasks);
|
|
4788
5022
|
const clock = contents.settings.clock;
|
|
4789
5023
|
return [
|
|
4790
5024
|
`${date} \u2014 ${agenda.all.length} occurrence${agenda.all.length === 1 ? "" : "s"}`,
|
|
@@ -4814,7 +5048,10 @@ function listRange(contents, from, to) {
|
|
|
4814
5048
|
let truncated = false;
|
|
4815
5049
|
for (let i = 0; i < span && !truncated; i++) {
|
|
4816
5050
|
const day = addDays(fromDay, i);
|
|
4817
|
-
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());
|
|
4818
5055
|
if (occs.length === 0) continue;
|
|
4819
5056
|
lines.push(`${dayKey(day)}:`);
|
|
4820
5057
|
for (const occ of occs) {
|
|
@@ -4831,16 +5068,29 @@ function listRange(contents, from, to) {
|
|
|
4831
5068
|
}
|
|
4832
5069
|
function searchEvents(contents, query, limit = 20) {
|
|
4833
5070
|
const clock = contents.settings.clock;
|
|
4834
|
-
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);
|
|
4835
5081
|
return [
|
|
4836
5082
|
`"${query}" \u2014 ${scored.length} match${scored.length === 1 ? "" : "es"}`,
|
|
4837
|
-
|
|
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)"]
|
|
4838
5088
|
].join("\n");
|
|
4839
5089
|
}
|
|
4840
5090
|
function freeSlots(contents, date, minutes, from, to) {
|
|
4841
5091
|
assertDateKey(date);
|
|
4842
5092
|
const day = fromDayKey(date);
|
|
4843
|
-
const occs =
|
|
5093
|
+
const occs = dayAgenda(contents.events, contents.calendars, day, contents.tasks).all;
|
|
4844
5094
|
const fromMin = from !== void 0 ? parseTime(from, "from") : contents.settings.wakingFrom ?? DEFAULT_WAKING.from;
|
|
4845
5095
|
const toMin = to !== void 0 ? parseTime(to, "to") : contents.settings.wakingTo ?? DEFAULT_WAKING.to;
|
|
4846
5096
|
const clock = contents.settings.clock;
|
|
@@ -4861,24 +5111,56 @@ function listCalendars(contents) {
|
|
|
4861
5111
|
}
|
|
4862
5112
|
function describeEvent(contents, id) {
|
|
4863
5113
|
const event = contents.events.find((e) => e.id === id);
|
|
4864
|
-
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
|
+
}
|
|
4865
5119
|
const calendar = contents.calendars.find((c) => c.id === event.calendarId);
|
|
4866
5120
|
const clock = contents.settings.clock;
|
|
4867
5121
|
return [
|
|
4868
5122
|
`${event.title || "Untitled"} (id=${event.id})`,
|
|
4869
5123
|
`Calendar: ${calendarName(contents.calendars, event.calendarId)}${calendar?.feed ? " (subscription, read-only)" : ""}`,
|
|
4870
|
-
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
|
+
] : [],
|
|
4871
5130
|
`Repeats: ${repeats(event) ? describeRule(event) : "never"}`,
|
|
5131
|
+
...event.remindLead !== REMIND_INHERIT ? [`Remind: ${event.remindLead === REMIND_SILENT ? "silent" : remindLabel(event.remindLead)}`] : [],
|
|
4872
5132
|
...event.location ? [`Location: ${event.location}`] : [],
|
|
4873
5133
|
...event.url ? [`URL: ${event.url}`] : [],
|
|
4874
5134
|
...event.notes ? [`Notes: ${event.notes}`] : [],
|
|
4875
|
-
...event.seriesId ? [`Overrides one occurrence of id=${event.seriesId} (${event.seriesKey})`] : []
|
|
4876
|
-
...event.task ? [
|
|
4877
|
-
`Done: ${event.doneKeys.length} occurrence${event.doneKeys.length === 1 ? "" : "s"} ticked`
|
|
4878
|
-
] : []
|
|
5135
|
+
...event.seriesId ? [`Overrides one occurrence of id=${event.seriesId} (${event.seriesKey})`] : []
|
|
4879
5136
|
].join("\n");
|
|
4880
5137
|
}
|
|
4881
|
-
function
|
|
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`
|
|
5158
|
+
].join("\n");
|
|
5159
|
+
}
|
|
5160
|
+
function defaultedSuffix(defaulted) {
|
|
5161
|
+
return defaulted ? " \u2014 calendar defaulted, not named" : "";
|
|
5162
|
+
}
|
|
5163
|
+
function performQuickAdd(contents, text, date, pin) {
|
|
4882
5164
|
if (!text.trim()) throw new RefusalError('"text" is empty');
|
|
4883
5165
|
const reference = date ? parseDay(date) : startOfDay(/* @__PURE__ */ new Date());
|
|
4884
5166
|
const parsed = parseQuickAdd(text, reference, /* @__PURE__ */ new Date(), contents.settings.defaultDuration);
|
|
@@ -4887,7 +5169,7 @@ function performQuickAdd(contents, text, date) {
|
|
|
4887
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"`
|
|
4888
5170
|
);
|
|
4889
5171
|
}
|
|
4890
|
-
const calendar = resolveCalendar(void 0, contents.calendars, contents.settings);
|
|
5172
|
+
const calendar = resolveCalendar(void 0, contents.calendars, contents.settings, pin);
|
|
4891
5173
|
const record = validate({
|
|
4892
5174
|
...draftEvent(parsed.day, parsed.startMin, parsed.endMin, calendar.id),
|
|
4893
5175
|
title: parsed.title,
|
|
@@ -4896,20 +5178,182 @@ function performQuickAdd(contents, text, date) {
|
|
|
4896
5178
|
const event = { ...record, id: newId(), updatedAt: Date.now() };
|
|
4897
5179
|
return {
|
|
4898
5180
|
mutated: { ...carried(contents), events: [...contents.events, event] },
|
|
4899
|
-
message: `Created ${eventSummaryLine(event, contents.calendars, contents.settings.clock)}`
|
|
5181
|
+
message: `Created ${eventSummaryLine(event, contents.calendars, contents.settings.clock)}` + defaultedSuffix(defaultedCalendar(void 0, pin))
|
|
4900
5182
|
};
|
|
4901
5183
|
}
|
|
4902
|
-
|
|
4903
|
-
|
|
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);
|
|
4904
5211
|
const event = { ...record, id: newId(), updatedAt: Date.now() };
|
|
4905
5212
|
return {
|
|
4906
5213
|
mutated: { ...carried(contents), events: [...contents.events, event] },
|
|
4907
|
-
message: `Created ${eventSummaryLine(event, contents.calendars, contents.settings.clock)}`
|
|
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))
|
|
4908
5279
|
};
|
|
4909
5280
|
}
|
|
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
|
+
});
|
|
5300
|
+
return {
|
|
5301
|
+
mutated: {
|
|
5302
|
+
...carried(contents),
|
|
5303
|
+
tasks: contents.tasks.map((t) => t.id === id ? next : t)
|
|
5304
|
+
},
|
|
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)}`
|
|
5328
|
+
};
|
|
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
|
+
}
|
|
4910
5349
|
function performUpdateEvent(contents, id, scopeArg, occurrence, patch) {
|
|
4911
5350
|
const original = contents.events.find((e) => e.id === id);
|
|
4912
|
-
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
|
+
}
|
|
4913
5357
|
refuseIfFeed(original, contents.calendars);
|
|
4914
5358
|
const scope = requireScope(original, scopeArg);
|
|
4915
5359
|
const clock = contents.settings.clock;
|
|
@@ -4951,7 +5395,10 @@ function performUpdateEvent(contents, id, scopeArg, occurrence, patch) {
|
|
|
4951
5395
|
}
|
|
4952
5396
|
function performDeleteEvent(contents, id, scopeArg, occurrence) {
|
|
4953
5397
|
const original = contents.events.find((e) => e.id === id);
|
|
4954
|
-
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
|
+
}
|
|
4955
5402
|
refuseIfFeed(original, contents.calendars);
|
|
4956
5403
|
const scope = requireScope(original, scopeArg);
|
|
4957
5404
|
const title = original.title || "Untitled";
|
|
@@ -4992,17 +5439,37 @@ function performDeleteEvent(contents, id, scopeArg, occurrence) {
|
|
|
4992
5439
|
message: `Deleted "${title}" from ${dayKey(new Date(at))} on.`
|
|
4993
5440
|
};
|
|
4994
5441
|
}
|
|
4995
|
-
function
|
|
4996
|
-
const
|
|
4997
|
-
if (!
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
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
|
+
);
|
|
5003
5470
|
return {
|
|
5004
|
-
mutated: { ...carried(contents),
|
|
5005
|
-
message: `${
|
|
5471
|
+
mutated: { ...carried(contents), tasks },
|
|
5472
|
+
message: `${original.done ? "Reopened" : "Completed"} "${original.title || "Untitled"}".`
|
|
5006
5473
|
};
|
|
5007
5474
|
}
|
|
5008
5475
|
function performCreateCalendar(contents, name, color, icon) {
|
|
@@ -5079,13 +5546,12 @@ function eventFieldsShape() {
|
|
|
5079
5546
|
return {
|
|
5080
5547
|
title: z.string().optional(),
|
|
5081
5548
|
date: dateKey("YYYY-MM-DD, local.").optional(),
|
|
5082
|
-
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(),
|
|
5083
5550
|
end: z.string().describe(
|
|
5084
5551
|
"HH:MM for a timed event, or YYYY-MM-DD (inclusive) for a multi-day all-day event. Alternative to duration."
|
|
5085
5552
|
).optional(),
|
|
5086
5553
|
duration: z.number().describe("Minutes \u2014 an alternative to end, for a timed event.").optional(),
|
|
5087
5554
|
allDay: z.boolean().describe("Forces an all-day event. Inferred when start, end and duration are all omitted.").optional(),
|
|
5088
|
-
task: z.boolean().describe("A dated to-do (one moment, not a span) rather than an event.").optional(),
|
|
5089
5555
|
calendarId: z.string().describe(
|
|
5090
5556
|
"Defaults to the user's default calendar. Must not be a subscription \u2014 those are read-only."
|
|
5091
5557
|
).optional(),
|
|
@@ -5094,29 +5560,65 @@ function eventFieldsShape() {
|
|
|
5094
5560
|
location: z.string().optional(),
|
|
5095
5561
|
url: z.string().optional(),
|
|
5096
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(),
|
|
5097
5569
|
recurrence: recurrenceSchema.optional()
|
|
5098
5570
|
};
|
|
5099
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
|
+
);
|
|
5100
5575
|
var createEventFieldsSchema = z.strictObject({
|
|
5101
5576
|
...eventFieldsShape(),
|
|
5102
|
-
date: dateKey("YYYY-MM-DD, local.")
|
|
5577
|
+
date: dateKey("YYYY-MM-DD, local."),
|
|
5578
|
+
key: upsertKeySchema.optional()
|
|
5103
5579
|
});
|
|
5104
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());
|
|
5105
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.';
|
|
5106
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".';
|
|
5107
5605
|
var scopeSchema = z.enum(["one", "future", "all"]).describe(SCOPE_DESCRIPTION);
|
|
5108
5606
|
var occurrenceSchema = z.string().describe(OCCURRENCE_DESCRIPTION);
|
|
5109
|
-
|
|
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) {
|
|
5110
5609
|
server.registerTool(
|
|
5111
5610
|
"list_day",
|
|
5112
5611
|
{
|
|
5113
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.",
|
|
5114
|
-
inputSchema: z.strictObject({
|
|
5613
|
+
inputSchema: z.strictObject({
|
|
5614
|
+
date: dateKey("YYYY-MM-DD, local."),
|
|
5615
|
+
calendar: calendarFilterSchema.optional()
|
|
5616
|
+
}),
|
|
5115
5617
|
annotations: { readOnlyHint: true }
|
|
5116
5618
|
},
|
|
5117
|
-
async ({ date }) => guarded(async () => {
|
|
5619
|
+
async ({ date, calendar }) => guarded(async () => {
|
|
5118
5620
|
const { contents, warning } = await store3.load();
|
|
5119
|
-
return withWarning(listDay(contents, date), warning);
|
|
5621
|
+
return withWarning(listDay(scopedToCalendar(contents, calendar), date), warning);
|
|
5120
5622
|
})
|
|
5121
5623
|
);
|
|
5122
5624
|
server.registerTool(
|
|
@@ -5125,28 +5627,33 @@ function registerTools(server, store3) {
|
|
|
5125
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.`,
|
|
5126
5628
|
inputSchema: z.strictObject({
|
|
5127
5629
|
from: dateKey("YYYY-MM-DD, local, inclusive."),
|
|
5128
|
-
to: dateKey("YYYY-MM-DD, local, inclusive.")
|
|
5630
|
+
to: dateKey("YYYY-MM-DD, local, inclusive."),
|
|
5631
|
+
calendar: calendarFilterSchema.optional()
|
|
5129
5632
|
}),
|
|
5130
5633
|
annotations: { readOnlyHint: true }
|
|
5131
5634
|
},
|
|
5132
|
-
async ({ from, to }) => guarded(async () => {
|
|
5635
|
+
async ({ from, to, calendar }) => guarded(async () => {
|
|
5133
5636
|
const { contents, warning } = await store3.load();
|
|
5134
|
-
return withWarning(listRange(contents, from, to), warning);
|
|
5637
|
+
return withWarning(listRange(scopedToCalendar(contents, calendar), from, to), warning);
|
|
5135
5638
|
})
|
|
5136
5639
|
);
|
|
5137
5640
|
server.registerTool(
|
|
5138
5641
|
"search",
|
|
5139
5642
|
{
|
|
5140
|
-
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.",
|
|
5141
5644
|
inputSchema: z.strictObject({
|
|
5142
5645
|
query: z.string(),
|
|
5143
|
-
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()
|
|
5144
5648
|
}),
|
|
5145
5649
|
annotations: { readOnlyHint: true }
|
|
5146
5650
|
},
|
|
5147
|
-
async ({ query, limit }) => guarded(async () => {
|
|
5651
|
+
async ({ query, limit, calendar }) => guarded(async () => {
|
|
5148
5652
|
const { contents, warning } = await store3.load();
|
|
5149
|
-
return withWarning(
|
|
5653
|
+
return withWarning(
|
|
5654
|
+
searchEvents(scopedToCalendar(contents, calendar), query, limit),
|
|
5655
|
+
warning
|
|
5656
|
+
);
|
|
5150
5657
|
})
|
|
5151
5658
|
);
|
|
5152
5659
|
server.registerTool(
|
|
@@ -5157,13 +5664,17 @@ function registerTools(server, store3) {
|
|
|
5157
5664
|
date: dateKey("YYYY-MM-DD, local."),
|
|
5158
5665
|
minutes: z.number().describe("Minimum slot length, in minutes."),
|
|
5159
5666
|
from: z.string().describe("HH:MM, local. Defaults to the user's waking-hours start.").optional(),
|
|
5160
|
-
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()
|
|
5161
5669
|
}),
|
|
5162
5670
|
annotations: { readOnlyHint: true }
|
|
5163
5671
|
},
|
|
5164
|
-
async ({ date, minutes, from, to }) => guarded(async () => {
|
|
5672
|
+
async ({ date, minutes, from, to, calendar }) => guarded(async () => {
|
|
5165
5673
|
const { contents, warning } = await store3.load();
|
|
5166
|
-
return withWarning(
|
|
5674
|
+
return withWarning(
|
|
5675
|
+
freeSlots(scopedToCalendar(contents, calendar), date, minutes, from, to),
|
|
5676
|
+
warning
|
|
5677
|
+
);
|
|
5167
5678
|
})
|
|
5168
5679
|
);
|
|
5169
5680
|
server.registerTool(
|
|
@@ -5204,7 +5715,7 @@ function registerTools(server, store3) {
|
|
|
5204
5715
|
},
|
|
5205
5716
|
async ({ text, date }) => guarded(
|
|
5206
5717
|
() => store3.write((contents) => {
|
|
5207
|
-
const outcome = performQuickAdd(contents, text, date);
|
|
5718
|
+
const outcome = performQuickAdd(contents, text, date, pin);
|
|
5208
5719
|
return { result: outcome.mutated, summary: outcome.message };
|
|
5209
5720
|
})
|
|
5210
5721
|
)
|
|
@@ -5212,13 +5723,30 @@ function registerTools(server, store3) {
|
|
|
5212
5723
|
server.registerTool(
|
|
5213
5724
|
"create_event",
|
|
5214
5725
|
{
|
|
5215
|
-
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.",
|
|
5216
5727
|
inputSchema: createEventFieldsSchema,
|
|
5217
5728
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
5218
5729
|
},
|
|
5219
5730
|
async (input) => guarded(
|
|
5220
5731
|
() => store3.write((contents) => {
|
|
5221
|
-
const
|
|
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);
|
|
5222
5750
|
return { result: outcome.mutated, summary: outcome.message };
|
|
5223
5751
|
})
|
|
5224
5752
|
)
|
|
@@ -5260,13 +5788,27 @@ function registerTools(server, store3) {
|
|
|
5260
5788
|
})
|
|
5261
5789
|
)
|
|
5262
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
|
+
);
|
|
5263
5805
|
server.registerTool(
|
|
5264
5806
|
"toggle_done",
|
|
5265
5807
|
{
|
|
5266
|
-
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.",
|
|
5267
5809
|
inputSchema: z.strictObject({
|
|
5268
5810
|
id: z.string(),
|
|
5269
|
-
occurrence: z.string().describe("
|
|
5811
|
+
occurrence: z.string().describe("Accepted for compatibility and ignored \u2014 a task is ticked whole.").optional()
|
|
5270
5812
|
}),
|
|
5271
5813
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
5272
5814
|
},
|
|
@@ -5277,6 +5819,66 @@ function registerTools(server, store3) {
|
|
|
5277
5819
|
})
|
|
5278
5820
|
)
|
|
5279
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
|
+
);
|
|
5280
5882
|
server.registerTool(
|
|
5281
5883
|
"create_calendar",
|
|
5282
5884
|
{
|
|
@@ -5295,6 +5897,18 @@ function registerTools(server, store3) {
|
|
|
5295
5897
|
})
|
|
5296
5898
|
)
|
|
5297
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
|
+
);
|
|
5298
5912
|
}
|
|
5299
5913
|
|
|
5300
5914
|
// mcp/index.ts
|
|
@@ -5304,6 +5918,10 @@ Usage:
|
|
|
5304
5918
|
circal-mcp --file <path> Start the server against a mirror file
|
|
5305
5919
|
circal-mcp --cloud Start the server against the cloud copy
|
|
5306
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
|
|
5307
5925
|
circal-mcp login Sign in to the cloud copy and remember the
|
|
5308
5926
|
derived key on this machine (interactive:
|
|
5309
5927
|
email, emailed code, passphrase; asks circal.day
|
|
@@ -5317,6 +5935,7 @@ Usage:
|
|
|
5317
5935
|
|
|
5318
5936
|
Environment:
|
|
5319
5937
|
CIRCAL_FILE Mirror file path, same as --file
|
|
5938
|
+
CIRCAL_CALENDAR Default calendar for writes, same as --calendar
|
|
5320
5939
|
CIRCAL_CLOUD_CREDENTIALS Where \`login\` keeps its record (default:
|
|
5321
5940
|
the platform config dir, circal/cloud.json)
|
|
5322
5941
|
CIRCAL_ALLOW_ZONE_MISMATCH Set to 1 to allow writes when this process's
|
|
@@ -5363,9 +5982,11 @@ if (argv.includes("--cloud")) {
|
|
|
5363
5982
|
}
|
|
5364
5983
|
store2 = fileStore({ path, env: process.env });
|
|
5365
5984
|
}
|
|
5985
|
+
var calendarFlag = argv.indexOf("--calendar");
|
|
5986
|
+
var pinned = (calendarFlag >= 0 ? argv[calendarFlag + 1] : void 0) || process.env.CIRCAL_CALENDAR;
|
|
5366
5987
|
function createServer() {
|
|
5367
5988
|
const server = new McpServer({ name: "circal", version: "1.0.0" });
|
|
5368
|
-
registerTools(server, store2);
|
|
5989
|
+
registerTools(server, store2, pinned);
|
|
5369
5990
|
return server;
|
|
5370
5991
|
}
|
|
5371
5992
|
void serveStdio(createServer);
|