@chibitek/chibi 0.1.173 → 0.1.175
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/dist/assets/{abnfDiagram-O67JEVCF-DaA9WpZw.js → abnfDiagram-O67JEVCF-CjXTIXBz.js} +1 -1
- package/dist/assets/{arc-DJ4Kdvv6.js → arc-1QgG2iO9.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-B975BUTv.js → architectureDiagram-NJMV4G6O-CzCo4hRJ.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CXOOfLy7.js → blockDiagram-BEXU5L5S-DhrHIUjL.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-CorqQ94E.js → c4Diagram-YGBWAQC7-gOjFoY7Z.js} +1 -1
- package/dist/assets/channel-DB0DMkPD.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-CPFHpKez.js → chunk-2Q5K7J3B-B9bsMZTt.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CrzBuB3A.js → chunk-3FUC2YCW-D6qIOfLe.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-DXJTBdvO.js → chunk-5VM5RSS4-DCixOiOi.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-B0TxALG4.js → chunk-GTNCS2PH-Bh52Az-G.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BL44uVOP.js → chunk-GWA4HPMP-Dbs4EZ88.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CxSklH7P.js → chunk-JWPE2WC7-jReoeHz2.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-_Lb-D8AY.js → chunk-NETBCI7D-CF3H0lZo.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-Dz5jp0xb.js → chunk-WEXAMYUT-DetCJmJX.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-C-cd9N0r.js → chunk-XXDRQBXY-CigsN-8U.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DtQ1781-.js → classDiagram-v2-NBCMYWYE-D8ZcMUWM.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-CHzWXNdH.js → cose-bilkent-JH36ORCC-BnqFoYIa.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-lHbKcf1S.js → cynefin-OW5HDTMX-DWSSGPqP.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BxY6MCKD.js → cynefinDiagram-VND7K2PF-Doo-ZQ5E.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-C2SBw-Mo.js → dagre-6A5THRUB-Bje_N-ue.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BwIAlmAU.js → diagram-22UHCM2B-osEfxfGj.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-i6fBkckD.js → diagram-3UASUU5V-D0ybINw1.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DwedCbB_.js → diagram-ATOU4E4O-DwS59waF.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-BISF-AQC.js → diagram-CDSNMT55-stDTabsA.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-ImgdRRxp.js → diagram-MLGK6HIB-COdtBBj4.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CxlAwPCS.js → diagram-MPIPVDR6-S32r0-4f.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-sVJkymKa.js → ebnfDiagram-ZINNZB2B-CZxbMOzV.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DU1vyX4w.js → elk-276RUBZZ-PzYOam0u.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CnCmL1Ve.js → erDiagram-OPXOYQCR-D0HBOTqF.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-DME2uLJf.js → flowDiagram-KWPJA3E3-1d8rftXL.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-CJQNW9ts.js → ganttDiagram-FUAMR5RP-CWn83VOd.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-D88_Y9tg.js → gitGraphDiagram-X574FWY7-CkLyRxvr.js} +1 -1
- package/dist/assets/{index-U9LN4GhF.js → index-BcBNl1Sc.js} +175 -175
- package/dist/assets/index-D17LLrYN.css +1 -0
- package/dist/assets/{index-ChmZZe7i.js → index-DfdDXEfq.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-CxWEf3NI.js → infoDiagram-VRGFBTTK-BmAsjOaa.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-B_YoNVjF.js → ishikawaDiagram-OU5B5YK6-B9d57hp_.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-ZYuBE2Bm.js → journeyDiagram-ZHPQQLJL-II3Kse3X.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-jmY_M4Qf.js → kanban-definition-PNTS6WVX-DtovCoCx.js} +1 -1
- package/dist/assets/{linear-kKUF6ayN.js → linear-BnH8vI1-.js} +1 -1
- package/dist/assets/{mermaid.core-DC5051S_.js → mermaid.core-BExpGmRM.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-C5knfygJ.js → mindmap-definition-NLK3R4M7-CSGJyaor.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-Ex6iedL2.js → pegDiagram-GJSIUBJH-BFgKWA7i.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-Dv38Ox0D.js → pieDiagram-5QR66LMP-Dl3FAvDB.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CwOEor8N.js → quadrantDiagram-O4NWA36T-DafRWmMB.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-DVgqz5MR.js → railroadDiagram-XR7U4H2S-CHJ7SyyJ.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BxiuYo52.js → requirementDiagram-PLB6GJNP-DJzpaN8z.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF--vq_1flP.js → sankeyDiagram-IPEJSGJF-BKqi0qaK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-9hLpfPA6.js → sequenceDiagram-PO4LG4MO-Bdc2LKBx.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BeGMkj9s.js → sizeCapture-INFHLROL-CIuw2Jav.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BnNOMugb.js → stateDiagram-v2-GCMORJYK-CocMnCmt.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-C1YG6Jq-.js → swimlanes-2SLR337P-D7Kw4SfT.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-Bq7WUTaZ.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-Co7OtwGA.js → timeline-definition-EJHVYXUP-DEXnhdui.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-BYHFARv3.js → usecaseDiagram-POWQR4AR-CJTa0bWM.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C-sdhL3h.js → vennDiagram-UO4OBE2U-CKphfio6.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-wf8ZNrmQ.js → wardleyDiagram-VNRHLVJA-COQ5I4oG.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BjRlkVGv.js → xychartDiagram-PMCCYNJV-BqlgNUIm.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +1 -0
- package/dist-server/index.js +1305 -747
- package/dist-server/openmausbot.js +3 -2
- package/dist-server/pair-cli.js +3 -2
- package/dist-server/server/about-me-sync.js +172 -0
- package/dist-server/server/config.js +1 -1
- package/dist-server/server/dictation.js +8 -0
- package/dist-server/server/drivers/claude.js +4 -4
- package/dist-server/server/icm-context.js +0 -22
- package/dist-server/server/index.js +26 -31
- package/dist-server/server/oauth-account-core.js +153 -0
- package/dist-server/server/oauth-account-tools.js +7 -348
- package/dist-server/server/oauth-calendar-tools.js +551 -0
- package/dist-server/server/paw/service.js +2 -1
- package/dist-server/server/paw/workspace.js +2 -1
- package/dist-server/server/vault/routes.js +20 -0
- package/dist-server/server/vault/store.js +24 -0
- package/dist-server/shared/oauth-providers.js +2 -1
- package/dist-server/shared/vault-csv.js +94 -0
- package/dist-server/vps-container-mcp.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-sBYHnjxz.js +0 -1
- package/dist/assets/index-COwCaN7p.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-9DghUs7S.js +0 -8
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
// Google Calendar and Outlook calendar tools: list, create, read, change and
|
|
2
|
+
// cancel events, including one occurrence, this and following, or a series.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { GRAPH, READS, SENDS, ToolInputError, accountArg, addresses, base, bounded, call, graphRecipient, json, noNewlines, parse, recipient, } from "./oauth-account-core.js";
|
|
5
|
+
const CALENDAR = "https://www.googleapis.com/calendar/v3";
|
|
6
|
+
function isoInstant(value) {
|
|
7
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value));
|
|
8
|
+
}
|
|
9
|
+
function isoDateOrTime(value) {
|
|
10
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(value) || /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})?$/.test(value);
|
|
11
|
+
}
|
|
12
|
+
const listEventsArgs = base.extend({
|
|
13
|
+
timeMin: z.string().refine(isoInstant, "timeMin must be an ISO 8601 date-time with a time zone, e.g. 2026-10-01T00:00:00Z"),
|
|
14
|
+
timeMax: z.string().refine(isoInstant, "timeMax must be an ISO 8601 date-time with a time zone, e.g. 2026-10-08T00:00:00Z"),
|
|
15
|
+
calendarId: z.string().min(1).max(320).optional(),
|
|
16
|
+
max: z.number().int().min(1).max(100).optional(),
|
|
17
|
+
}).strict();
|
|
18
|
+
const createEventArgs = base.extend({
|
|
19
|
+
summary: z.string().min(1).max(1_000).refine(noNewlines, "summary must be one line"),
|
|
20
|
+
start: z.string().refine(isoDateOrTime, "start must be YYYY-MM-DD or an ISO 8601 date-time"),
|
|
21
|
+
end: z.string().refine(isoDateOrTime, "end must be YYYY-MM-DD or an ISO 8601 date-time"),
|
|
22
|
+
timeZone: z.string().min(1).max(64).regex(/^[A-Za-z0-9_+\-/ ]+$/).optional(),
|
|
23
|
+
description: z.string().max(8_000).optional(),
|
|
24
|
+
location: z.string().max(1_000).optional(),
|
|
25
|
+
attendees: z.array(z.string()).max(50).optional(),
|
|
26
|
+
calendarId: z.string().min(1).max(320).optional(),
|
|
27
|
+
}).strict();
|
|
28
|
+
const eventArgs = base.extend({ id: z.string().min(1).max(512).regex(/^[A-Za-z0-9!_.=+\-/~:]+$/, "not a valid id"), calendarId: z.string().min(1).max(320).optional() }).strict();
|
|
29
|
+
const scopeArg = z.enum(["occurrence", "following", "series"]).optional();
|
|
30
|
+
const notifyArg = z.enum(["all", "externalOnly", "none"]);
|
|
31
|
+
const updateEventArgs = eventArgs.extend({
|
|
32
|
+
scope: scopeArg,
|
|
33
|
+
notify: notifyArg,
|
|
34
|
+
summary: createEventArgs.shape.summary.optional(),
|
|
35
|
+
start: createEventArgs.shape.start.optional(),
|
|
36
|
+
end: createEventArgs.shape.end.optional(),
|
|
37
|
+
timeZone: createEventArgs.shape.timeZone,
|
|
38
|
+
description: createEventArgs.shape.description,
|
|
39
|
+
location: createEventArgs.shape.location,
|
|
40
|
+
attendees: z.array(z.string()).max(50).optional(),
|
|
41
|
+
addAttendees: z.array(z.string()).max(50).optional(),
|
|
42
|
+
removeAttendees: z.array(z.string()).max(50).optional(),
|
|
43
|
+
room: z.string().max(320).optional(),
|
|
44
|
+
}).strict();
|
|
45
|
+
const deleteEventArgs = eventArgs.extend({ scope: scopeArg, notify: notifyArg }).strict();
|
|
46
|
+
/** Graph has no way to change a meeting quietly: the organizer's edits
|
|
47
|
+
* always reach the guests, so Outlook tools only accept notify "all". */
|
|
48
|
+
const outlookUpdateEventArgs = updateEventArgs.omit({ calendarId: true }).extend({ notify: z.literal("all") }).strict();
|
|
49
|
+
const outlookDeleteEventArgs = deleteEventArgs.omit({ calendarId: true }).extend({ notify: z.literal("all") }).strict();
|
|
50
|
+
const listEventsSchema = (withCalendar) => ({
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
account: accountArg,
|
|
54
|
+
timeMin: { type: "string", description: "Start of the window, ISO 8601 with a time zone (e.g. 2026-10-01T00:00:00Z)." },
|
|
55
|
+
timeMax: { type: "string", description: "End of the window, ISO 8601 with a time zone." },
|
|
56
|
+
...(withCalendar ? { calendarId: { type: "string", description: "Calendar id; defaults to the primary calendar." } } : {}),
|
|
57
|
+
max: { type: "integer", minimum: 1, maximum: 100, description: "Most events to return (default 25)." },
|
|
58
|
+
},
|
|
59
|
+
required: ["timeMin", "timeMax"],
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
});
|
|
62
|
+
const createEventSchema = (withCalendar) => ({
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
account: accountArg,
|
|
66
|
+
summary: { type: "string", description: "Event title." },
|
|
67
|
+
start: { type: "string", description: "YYYY-MM-DD for an all-day event, or an ISO 8601 date-time. Without an offset, pass timeZone." },
|
|
68
|
+
end: { type: "string", description: "Same format as start. For all-day events this is the day after the last day." },
|
|
69
|
+
timeZone: { type: "string", description: "IANA time zone such as America/Los_Angeles, used when start/end have no offset." },
|
|
70
|
+
description: { type: "string" },
|
|
71
|
+
location: { type: "string" },
|
|
72
|
+
attendees: { type: "array", items: { type: "string" }, description: "Guest email addresses. They receive an invitation." },
|
|
73
|
+
...(withCalendar ? { calendarId: { type: "string", description: "Calendar id; defaults to the primary calendar." } } : {}),
|
|
74
|
+
},
|
|
75
|
+
required: ["summary", "start", "end"],
|
|
76
|
+
additionalProperties: false,
|
|
77
|
+
});
|
|
78
|
+
const calendarProp = { calendarId: { type: "string", description: "Calendar id; defaults to the primary calendar." } };
|
|
79
|
+
const eventIdProp = { type: "string", description: "Event id from the list tool: one occurrence of a recurring event, or a single event." };
|
|
80
|
+
const scopeProp = {
|
|
81
|
+
type: "string",
|
|
82
|
+
enum: ["occurrence", "following", "series"],
|
|
83
|
+
description: "For a recurring event: this occurrence only (default), this and following, or the whole series. Pass the occurrence's id either way; the series is found from it.",
|
|
84
|
+
};
|
|
85
|
+
const notifyProp = (outlook) => outlook
|
|
86
|
+
? { type: "string", enum: ["all"], description: "Outlook always tells the guests about the organizer's changes; pass all to go ahead." }
|
|
87
|
+
: { type: "string", enum: ["all", "externalOnly", "none"], description: "Who gets an email about the change: all guests, only guests outside your organization, or nobody." };
|
|
88
|
+
const eventSchema = (withCalendar) => ({
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: { account: accountArg, id: eventIdProp, ...(withCalendar ? calendarProp : {}) },
|
|
91
|
+
required: ["id"],
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
});
|
|
94
|
+
const updateEventSchema = (withCalendar) => ({
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
account: accountArg,
|
|
98
|
+
id: eventIdProp,
|
|
99
|
+
...(withCalendar ? calendarProp : {}),
|
|
100
|
+
scope: scopeProp,
|
|
101
|
+
notify: notifyProp(!withCalendar),
|
|
102
|
+
summary: { type: "string", description: "New title." },
|
|
103
|
+
description: { type: "string", description: "New description; replaces the old one." },
|
|
104
|
+
location: { type: "string" },
|
|
105
|
+
start: { type: "string", description: "New start, same format as when creating. Pass start and end together. To move a whole series, give a UTC offset." },
|
|
106
|
+
end: { type: "string", description: "New end, same format as start." },
|
|
107
|
+
timeZone: { type: "string", description: "IANA time zone for start/end without an offset." },
|
|
108
|
+
attendees: { type: "array", items: { type: "string" }, description: "Replace the whole guest list with these addresses. Guests who stay keep their reply." },
|
|
109
|
+
addAttendees: { type: "array", items: { type: "string" }, description: "Guests to add." },
|
|
110
|
+
removeAttendees: { type: "array", items: { type: "string" }, description: "Guests to remove." },
|
|
111
|
+
room: { type: "string", description: "Room or resource email address; replaces the room already booked." },
|
|
112
|
+
},
|
|
113
|
+
required: ["id", "notify"],
|
|
114
|
+
additionalProperties: false,
|
|
115
|
+
});
|
|
116
|
+
const deleteEventSchema = (withCalendar) => ({
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: { account: accountArg, id: eventIdProp, ...(withCalendar ? calendarProp : {}), scope: scopeProp, notify: notifyProp(!withCalendar) },
|
|
119
|
+
required: ["id", "notify"],
|
|
120
|
+
additionalProperties: false,
|
|
121
|
+
});
|
|
122
|
+
export const GOOGLE_CALENDAR_TOOLS = [
|
|
123
|
+
{ name: "calendar_list_events", provider: "google", family: "calendar", description: "List Google Calendar events between two times.", inputSchema: listEventsSchema(true), annotations: READS, args: listEventsArgs },
|
|
124
|
+
{ name: "calendar_create_event", provider: "google", family: "calendar", description: "Create a Google Calendar event. Attendees receive an invitation.", inputSchema: createEventSchema(true), annotations: SENDS, args: createEventArgs },
|
|
125
|
+
{ name: "calendar_get_event", provider: "google", family: "calendar", description: "Read one Google Calendar event by id: details, its series (recurringEventId, recurrence) and each guest's reply.", inputSchema: eventSchema(true), annotations: READS, args: eventArgs },
|
|
126
|
+
{ name: "calendar_update_event", provider: "google", family: "calendar", description: "Change a Google Calendar event: title, description, location, times, guests or room, for one occurrence, this and following, or the whole series. notify chooses who is emailed.", inputSchema: updateEventSchema(true), annotations: SENDS, args: updateEventArgs },
|
|
127
|
+
{ name: "calendar_delete_event", provider: "google", family: "calendar", description: "Cancel a Google Calendar event: one occurrence, this and following, or the whole series. notify chooses who is emailed.", inputSchema: deleteEventSchema(true), annotations: SENDS, args: deleteEventArgs },
|
|
128
|
+
];
|
|
129
|
+
export const OUTLOOK_CALENDAR_TOOLS = [
|
|
130
|
+
{ name: "outlook_calendar_list_events", provider: "microsoft", family: "outlookCalendar", description: "List Outlook calendar events between two times (times are returned in UTC).", inputSchema: listEventsSchema(false), annotations: READS, args: listEventsArgs.omit({ calendarId: true }) },
|
|
131
|
+
{ name: "outlook_calendar_create_event", provider: "microsoft", family: "outlookCalendar", description: "Create an Outlook calendar event. Attendees receive an invitation.", inputSchema: createEventSchema(false), annotations: SENDS, args: createEventArgs.omit({ calendarId: true }) },
|
|
132
|
+
{ name: "outlook_calendar_get_event", provider: "microsoft", family: "outlookCalendar", description: "Read one Outlook calendar event by id: details, its series (seriesMasterId, recurrence) and each guest's reply (times in UTC).", inputSchema: eventSchema(false), annotations: READS, args: eventArgs.omit({ calendarId: true }) },
|
|
133
|
+
{ name: "outlook_calendar_update_event", provider: "microsoft", family: "outlookCalendar", description: "Change an Outlook calendar event: title, description, location, times, guests or room, for one occurrence, this and following, or the whole series. Guests are told.", inputSchema: updateEventSchema(false), annotations: SENDS, args: outlookUpdateEventArgs },
|
|
134
|
+
{ name: "outlook_calendar_delete_event", provider: "microsoft", family: "outlookCalendar", description: "Cancel an Outlook calendar event: one occurrence, this and following, or the whole series. Guests are told.", inputSchema: deleteEventSchema(false), annotations: SENDS, args: outlookDeleteEventArgs },
|
|
135
|
+
];
|
|
136
|
+
function guestEdit(args) {
|
|
137
|
+
const replace = args.attendees === undefined ? undefined : addresses(args.attendees, "attendees");
|
|
138
|
+
const add = addresses(args.addAttendees, "addAttendees");
|
|
139
|
+
const remove = addresses(args.removeAttendees, "removeAttendees");
|
|
140
|
+
const room = args.room === undefined ? undefined : addresses(args.room, "room")[0];
|
|
141
|
+
if (replace && (add.length || remove.length))
|
|
142
|
+
throw new ToolInputError("Pass attendees to replace the guest list, or addAttendees/removeAttendees, not both");
|
|
143
|
+
return replace || add.length || remove.length || room ? { replace, add, remove, room } : null;
|
|
144
|
+
}
|
|
145
|
+
/** Apply a guest edit to a provider's attendee list. Guests who stay keep
|
|
146
|
+
* their original entry, so their reply survives. */
|
|
147
|
+
function editGuests(current, email, isRoom, make, edit) {
|
|
148
|
+
const same = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
149
|
+
const people = current.filter((guest) => !isRoom(guest));
|
|
150
|
+
let next;
|
|
151
|
+
if (edit.replace) {
|
|
152
|
+
next = edit.replace.map((address) => people.find((guest) => same(email(guest), address)) ?? make(address, false));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
for (const address of edit.remove) {
|
|
156
|
+
if (!people.some((guest) => same(email(guest), address)))
|
|
157
|
+
throw new ToolInputError(`removeAttendees: ${address} is not a guest of this event`);
|
|
158
|
+
}
|
|
159
|
+
next = people.filter((guest) => !edit.remove.some((address) => same(email(guest), address)));
|
|
160
|
+
for (const address of edit.add)
|
|
161
|
+
if (!next.some((guest) => same(email(guest), address)))
|
|
162
|
+
next.push(make(address, false));
|
|
163
|
+
}
|
|
164
|
+
return [...next, ...(edit.room ? [make(edit.room, true)] : current.filter(isRoom))];
|
|
165
|
+
}
|
|
166
|
+
function needsChange(args, fields) {
|
|
167
|
+
if (!Object.keys(fields).length && args.start === undefined && args.end === undefined)
|
|
168
|
+
throw new ToolInputError("Nothing to change: pass at least one field to update");
|
|
169
|
+
if ((args.start === undefined) !== (args.end === undefined))
|
|
170
|
+
throw new ToolInputError("Pass start and end together");
|
|
171
|
+
}
|
|
172
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
173
|
+
/** Milliseconds for a date, an instant, or a wall-clock time read as UTC. */
|
|
174
|
+
const millis = (value) => Date.parse(DATE.test(value) ? `${value}T00:00:00Z` : /(?:Z|[+-]\d{2}:\d{2})$/.test(value) ? value : `${value.slice(0, 19)}Z`);
|
|
175
|
+
/** Move a whole series as far as one of its occurrences is being moved:
|
|
176
|
+
* the series' first start, shifted, plus the new duration. */
|
|
177
|
+
function shiftSeries(seriesStart, occurrenceStart, allDay, start, end) {
|
|
178
|
+
if (DATE.test(start) !== allDay)
|
|
179
|
+
throw new ToolInputError("A whole series cannot switch between all-day and timed; change one occurrence or this and following");
|
|
180
|
+
if (!allDay && !/(?:Z|[+-]\d{2}:\d{2})$/.test(start))
|
|
181
|
+
throw new ToolInputError("start: to move a whole series, give a UTC offset (e.g. 2026-10-01T15:00:00-07:00)");
|
|
182
|
+
const moved = millis(seriesStart) + millis(start) - millis(occurrenceStart);
|
|
183
|
+
return [moved, moved + millis(end) - millis(start)];
|
|
184
|
+
}
|
|
185
|
+
function dayBefore(date) {
|
|
186
|
+
const day = new Date(`${date}T00:00:00Z`);
|
|
187
|
+
day.setUTCDate(day.getUTCDate() - 1);
|
|
188
|
+
return day.toISOString().slice(0, 10);
|
|
189
|
+
}
|
|
190
|
+
// ── Google ────────────────────────────────────────────────────────────────
|
|
191
|
+
const googleTime = z.object({ dateTime: z.string().optional(), date: z.string().optional(), timeZone: z.string().optional() });
|
|
192
|
+
// Loose, so a guest entry or a copied series keeps fields this file never reads.
|
|
193
|
+
const googleAttendee = z.object({ email: z.string().optional(), responseStatus: z.string().optional(), resource: z.boolean().optional() }).passthrough();
|
|
194
|
+
const googleEventSchema = z.object({
|
|
195
|
+
id: z.string(),
|
|
196
|
+
summary: z.string().optional(),
|
|
197
|
+
description: z.string().optional(),
|
|
198
|
+
location: z.string().optional(),
|
|
199
|
+
status: z.string().optional(),
|
|
200
|
+
htmlLink: z.string().optional(),
|
|
201
|
+
start: googleTime.optional(),
|
|
202
|
+
end: googleTime.optional(),
|
|
203
|
+
organizer: z.object({ email: z.string().optional() }).optional(),
|
|
204
|
+
attendees: z.array(googleAttendee).optional(),
|
|
205
|
+
recurringEventId: z.string().optional(),
|
|
206
|
+
originalStartTime: googleTime.optional(),
|
|
207
|
+
recurrence: z.array(z.string()).optional(),
|
|
208
|
+
}).passthrough();
|
|
209
|
+
function googleEvent(event) {
|
|
210
|
+
return {
|
|
211
|
+
id: event.id,
|
|
212
|
+
summary: event.summary,
|
|
213
|
+
start: event.start?.dateTime ?? event.start?.date,
|
|
214
|
+
end: event.end?.dateTime ?? event.end?.date,
|
|
215
|
+
location: event.location,
|
|
216
|
+
organizer: event.organizer?.email,
|
|
217
|
+
attendees: event.attendees?.map((attendee) => `${attendee.email ?? ""}${attendee.resource ? " (room)" : ""}${attendee.responseStatus ? ` (${attendee.responseStatus})` : ""}`),
|
|
218
|
+
description: event.description ? bounded(event.description, 2_000) : undefined,
|
|
219
|
+
recurringEventId: event.recurringEventId,
|
|
220
|
+
recurrence: event.recurrence,
|
|
221
|
+
link: event.htmlLink,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async function calendarListEvents(ctx, account, args) {
|
|
225
|
+
const calendarId = encodeURIComponent(args.calendarId ?? "primary");
|
|
226
|
+
const query = new URLSearchParams({ timeMin: args.timeMin, timeMax: args.timeMax, singleEvents: "true", orderBy: "startTime", maxResults: String(args.max ?? 25) });
|
|
227
|
+
const list = parse(z.object({ items: z.array(googleEventSchema).optional(), timeZone: z.string().optional() }), await call(ctx, account, `${CALENDAR}/calendars/${calendarId}/events?${query}`, { expect: "json" }), "Calendar");
|
|
228
|
+
return json({ account: account.email, timeZone: list.timeZone, events: (list.items ?? []).map(googleEvent) });
|
|
229
|
+
}
|
|
230
|
+
function eventTime(value, timeZone, field) {
|
|
231
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
232
|
+
return { allDay: true, date: value };
|
|
233
|
+
const hasOffset = /(?:Z|[+-]\d{2}:\d{2})$/.test(value);
|
|
234
|
+
if (!hasOffset && !timeZone)
|
|
235
|
+
throw new ToolInputError(`${field}: add a UTC offset (e.g. 2026-10-01T15:00:00-07:00) or pass timeZone`);
|
|
236
|
+
return { allDay: false, dateTime: value, hasOffset };
|
|
237
|
+
}
|
|
238
|
+
function googleTimes(startValue, endValue, timeZone) {
|
|
239
|
+
const start = eventTime(startValue, timeZone, "start");
|
|
240
|
+
const end = eventTime(endValue, timeZone, "end");
|
|
241
|
+
if (start.allDay !== end.allDay)
|
|
242
|
+
throw new ToolInputError("start and end must both be dates or both be date-times");
|
|
243
|
+
const time = (value) => value.allDay
|
|
244
|
+
? { date: value.date }
|
|
245
|
+
: { dateTime: value.dateTime, ...(timeZone ? { timeZone } : {}) };
|
|
246
|
+
return { start: time(start), end: time(end) };
|
|
247
|
+
}
|
|
248
|
+
async function calendarCreateEvent(ctx, account, args) {
|
|
249
|
+
const attendees = addresses(args.attendees, "attendees");
|
|
250
|
+
const body = {
|
|
251
|
+
summary: args.summary,
|
|
252
|
+
...(args.description ? { description: args.description } : {}),
|
|
253
|
+
...(args.location ? { location: args.location } : {}),
|
|
254
|
+
...googleTimes(args.start, args.end, args.timeZone),
|
|
255
|
+
...(attendees.length ? { attendees: attendees.map((email) => ({ email })) } : {}),
|
|
256
|
+
};
|
|
257
|
+
const calendarId = encodeURIComponent(args.calendarId ?? "primary");
|
|
258
|
+
const query = new URLSearchParams({ sendUpdates: attendees.length ? "all" : "none" });
|
|
259
|
+
const created = parse(googleEventSchema, await call(ctx, account, `${CALENDAR}/calendars/${calendarId}/events?${query}`, {
|
|
260
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), expect: "json",
|
|
261
|
+
}), "Calendar");
|
|
262
|
+
return json({ account: account.email, created: googleEvent(created) });
|
|
263
|
+
}
|
|
264
|
+
const eventUrl = (calendarId, id, query) => `${CALENDAR}/calendars/${encodeURIComponent(calendarId ?? "primary")}/events${id ? `/${encodeURIComponent(id)}` : ""}${query ? `?${new URLSearchParams(query)}` : ""}`;
|
|
265
|
+
const when = (time) => time?.dateTime ?? time?.date ?? "";
|
|
266
|
+
async function googleGet(ctx, account, calendarId, id) {
|
|
267
|
+
return parse(googleEventSchema, await call(ctx, account, eventUrl(calendarId, id), { expect: "json" }), "Calendar");
|
|
268
|
+
}
|
|
269
|
+
async function googleSave(ctx, account, method, url, body) {
|
|
270
|
+
return parse(googleEventSchema, await call(ctx, account, url, { method, headers: { "content-type": "application/json" }, body: JSON.stringify(body), expect: "json" }), "Calendar");
|
|
271
|
+
}
|
|
272
|
+
/** The event a scoped change lands on. An occurrence names its series in
|
|
273
|
+
* recurringEventId; after an earlier "this and following" split that is the
|
|
274
|
+
* newer series (an id ending in _R<date>), which is the one to change. */
|
|
275
|
+
async function googleTarget(ctx, account, args) {
|
|
276
|
+
const scope = args.scope ?? "occurrence";
|
|
277
|
+
const event = await googleGet(ctx, account, args.calendarId, args.id);
|
|
278
|
+
if (scope === "occurrence" && event.recurrence)
|
|
279
|
+
throw new ToolInputError(`${args.id} is a whole series: pass the id of one occurrence (from calendar_list_events), or scope "series"`);
|
|
280
|
+
if (scope === "occurrence" || (!event.recurringEventId && !event.recurrence))
|
|
281
|
+
return { scope: "occurrence", event, series: event };
|
|
282
|
+
const series = event.recurringEventId ? await googleGet(ctx, account, args.calendarId, event.recurringEventId) : event;
|
|
283
|
+
const first = !event.originalStartTime || millis(when(event.originalStartTime)) === millis(when(series.start));
|
|
284
|
+
return { scope: first ? "series" : scope, event, series };
|
|
285
|
+
}
|
|
286
|
+
function googleFields(args, current) {
|
|
287
|
+
const guests = guestEdit(args);
|
|
288
|
+
return {
|
|
289
|
+
...(args.summary !== undefined ? { summary: args.summary } : {}),
|
|
290
|
+
...(args.description !== undefined ? { description: args.description } : {}),
|
|
291
|
+
...(args.location !== undefined ? { location: args.location } : {}),
|
|
292
|
+
...(guests ? {
|
|
293
|
+
attendees: editGuests(current.attendees ?? [], (guest) => guest.email ?? "", (guest) => guest.resource === true, (email, resource) => (resource ? { email, resource } : { email }), guests),
|
|
294
|
+
} : {}),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/** End a series just before one occurrence: UNTIL replaces any COUNT. */
|
|
298
|
+
function capRecurrence(rules, from) {
|
|
299
|
+
const until = from.date ? dayBefore(from.date).replace(/-/g, "") : new Date(millis(from.dateTime ?? "") - 1_000).toISOString().replace(/[-:]|\.\d{3}/g, "");
|
|
300
|
+
return rules.map((rule) => rule.startsWith("RRULE:")
|
|
301
|
+
? `RRULE:${[...rule.slice(6).split(";").filter((part) => !/^(UNTIL|COUNT)=/i.test(part)), `UNTIL=${until}`].join(";")}`
|
|
302
|
+
: rule);
|
|
303
|
+
}
|
|
304
|
+
/** What a new "this and following" series copies from the old one. */
|
|
305
|
+
const SERIES_FIELDS = ["summary", "description", "location", "attendees", "colorId", "reminders", "transparency", "visibility", "guestsCanModify", "guestsCanInviteOthers", "guestsCanSeeOtherGuests"];
|
|
306
|
+
async function calendarGetEvent(ctx, account, args) {
|
|
307
|
+
return json({ account: account.email, event: googleEvent(await googleGet(ctx, account, args.calendarId, args.id)) });
|
|
308
|
+
}
|
|
309
|
+
async function calendarUpdateEvent(ctx, account, args) {
|
|
310
|
+
const { scope, event, series } = await googleTarget(ctx, account, args);
|
|
311
|
+
const fields = googleFields(args, series);
|
|
312
|
+
needsChange(args, fields);
|
|
313
|
+
const notify = { sendUpdates: args.notify };
|
|
314
|
+
if (scope === "occurrence") {
|
|
315
|
+
const times = args.start !== undefined ? googleTimes(args.start, args.end, args.timeZone) : {};
|
|
316
|
+
const updated = await googleSave(ctx, account, "PATCH", eventUrl(args.calendarId, event.id, notify), { ...fields, ...times });
|
|
317
|
+
return json({ account: account.email, scope, updated: googleEvent(updated) });
|
|
318
|
+
}
|
|
319
|
+
if (scope === "series") {
|
|
320
|
+
let times = {};
|
|
321
|
+
if (args.start !== undefined) {
|
|
322
|
+
const allDay = Boolean(event.start?.date);
|
|
323
|
+
const [start, end] = shiftSeries(when(series.start), when(event.start), allDay, args.start, args.end);
|
|
324
|
+
const at = (ms) => allDay ? { date: new Date(ms).toISOString().slice(0, 10) } : { dateTime: new Date(ms).toISOString(), timeZone: args.timeZone ?? series.start?.timeZone };
|
|
325
|
+
times = { start: at(start), end: at(end) };
|
|
326
|
+
}
|
|
327
|
+
const updated = await googleSave(ctx, account, "PATCH", eventUrl(args.calendarId, series.id, notify), { ...fields, ...times });
|
|
328
|
+
return json({ account: account.email, scope, updated: googleEvent(updated) });
|
|
329
|
+
}
|
|
330
|
+
// Google has no "this and following" edit: start a new series at this
|
|
331
|
+
// occurrence, then end the old one before it. Creating first means a
|
|
332
|
+
// failure leaves a duplicate to delete, never lost future meetings.
|
|
333
|
+
if (series.recurrence?.some((rule) => /^RRULE:.*\bCOUNT=/i.test(rule))) {
|
|
334
|
+
throw new ToolInputError("This series ends after a set number of times, so it cannot be split: change one occurrence or the whole series");
|
|
335
|
+
}
|
|
336
|
+
const copied = Object.fromEntries(SERIES_FIELDS.filter((key) => series[key] !== undefined).map((key) => [key, series[key]]));
|
|
337
|
+
const times = args.start !== undefined ? googleTimes(args.start, args.end, args.timeZone) : { start: event.start, end: event.end };
|
|
338
|
+
const created = await googleSave(ctx, account, "POST", eventUrl(args.calendarId, "", notify), { ...copied, ...fields, ...times, recurrence: series.recurrence });
|
|
339
|
+
await googleSave(ctx, account, "PATCH", eventUrl(args.calendarId, series.id, notify), { recurrence: capRecurrence(series.recurrence ?? [], event.originalStartTime ?? {}) });
|
|
340
|
+
return json({ account: account.email, scope, updated: googleEvent(created), endedSeries: series.id, note: "The old series now ends before this occurrence; a new series with the changes starts here." });
|
|
341
|
+
}
|
|
342
|
+
async function calendarDeleteEvent(ctx, account, args) {
|
|
343
|
+
const { scope, event, series } = await googleTarget(ctx, account, args);
|
|
344
|
+
const notify = { sendUpdates: args.notify };
|
|
345
|
+
if (scope === "following") {
|
|
346
|
+
await googleSave(ctx, account, "PATCH", eventUrl(args.calendarId, series.id, notify), { recurrence: capRecurrence(series.recurrence ?? [], event.originalStartTime ?? {}) });
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
await call(ctx, account, eventUrl(args.calendarId, (scope === "series" ? series : event).id, notify), { method: "DELETE", expect: "none" });
|
|
350
|
+
}
|
|
351
|
+
return json({ account: account.email, cancelled: true, scope, id: scope === "occurrence" ? event.id : series.id });
|
|
352
|
+
}
|
|
353
|
+
// ── Microsoft Graph ───────────────────────────────────────────────────────
|
|
354
|
+
const graphTime = z.object({ dateTime: z.string().optional(), timeZone: z.string().optional() });
|
|
355
|
+
const graphEventSchema = z.object({
|
|
356
|
+
id: z.string(),
|
|
357
|
+
subject: z.string().nullable().optional(),
|
|
358
|
+
start: graphTime.optional(),
|
|
359
|
+
end: graphTime.optional(),
|
|
360
|
+
isAllDay: z.boolean().optional(),
|
|
361
|
+
location: z.object({ displayName: z.string().optional() }).nullable().optional(),
|
|
362
|
+
organizer: graphRecipient.nullable().optional(),
|
|
363
|
+
attendees: z.array(graphRecipient.extend({ type: z.string().optional(), status: z.object({ response: z.string().optional() }).optional() })).optional(),
|
|
364
|
+
webLink: z.string().optional(),
|
|
365
|
+
bodyPreview: z.string().optional(),
|
|
366
|
+
body: z.object({ contentType: z.string().optional(), content: z.string().optional() }).optional(),
|
|
367
|
+
type: z.string().optional(),
|
|
368
|
+
seriesMasterId: z.string().nullable().optional(),
|
|
369
|
+
originalStartTimeZone: z.string().optional(),
|
|
370
|
+
recurrence: z.object({
|
|
371
|
+
pattern: z.record(z.string(), z.unknown()),
|
|
372
|
+
range: z.object({ type: z.string(), startDate: z.string().optional(), endDate: z.string().optional() }).passthrough(),
|
|
373
|
+
}).nullable().optional(),
|
|
374
|
+
});
|
|
375
|
+
function graphEvent(event) {
|
|
376
|
+
return {
|
|
377
|
+
id: event.id,
|
|
378
|
+
summary: event.subject,
|
|
379
|
+
start: event.start?.dateTime ? `${event.start.dateTime}${event.start.timeZone === "UTC" ? "Z" : ` (${event.start.timeZone ?? ""})`}` : undefined,
|
|
380
|
+
end: event.end?.dateTime ? `${event.end.dateTime}${event.end.timeZone === "UTC" ? "Z" : ` (${event.end.timeZone ?? ""})`}` : undefined,
|
|
381
|
+
allDay: event.isAllDay,
|
|
382
|
+
location: event.location?.displayName || undefined,
|
|
383
|
+
organizer: recipient(event.organizer),
|
|
384
|
+
attendees: event.attendees?.map((attendee) => `${recipient(attendee) ?? ""}${attendee.type === "resource" ? " (room)" : ""}${attendee.status?.response ? ` (${attendee.status.response})` : ""}`),
|
|
385
|
+
preview: event.bodyPreview ? bounded(event.bodyPreview, 1_000) : undefined,
|
|
386
|
+
type: event.type,
|
|
387
|
+
seriesMasterId: event.seriesMasterId ?? undefined,
|
|
388
|
+
recurrence: event.recurrence ?? undefined,
|
|
389
|
+
link: event.webLink,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async function outlookListEvents(ctx, account, args) {
|
|
393
|
+
const query = new URLSearchParams({
|
|
394
|
+
startDateTime: new Date(args.timeMin).toISOString(),
|
|
395
|
+
endDateTime: new Date(args.timeMax).toISOString(),
|
|
396
|
+
$top: String(args.max ?? 25),
|
|
397
|
+
$orderby: "start/dateTime",
|
|
398
|
+
$select: "id,subject,start,end,isAllDay,location,organizer,attendees,webLink,bodyPreview",
|
|
399
|
+
});
|
|
400
|
+
const list = parse(z.object({ value: z.array(graphEventSchema) }), await call(ctx, account, `${GRAPH}/me/calendarView?${query}`, {
|
|
401
|
+
headers: { prefer: 'outlook.timezone="UTC"' }, expect: "json",
|
|
402
|
+
}), "Outlook calendar");
|
|
403
|
+
return json({ account: account.email, timeZone: "UTC", events: list.value.map(graphEvent) });
|
|
404
|
+
}
|
|
405
|
+
/** Graph wants a wall-clock time plus a zone: a time with an offset is
|
|
406
|
+
* converted to UTC, one without keeps the zone the bot named. */
|
|
407
|
+
export function graphDateTime(value, timeZone, field) {
|
|
408
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
409
|
+
return { dateTime: `${value}T00:00:00`, timeZone: timeZone ?? "UTC", allDay: true };
|
|
410
|
+
if (/(?:Z|[+-]\d{2}:\d{2})$/.test(value)) {
|
|
411
|
+
const parsed = Date.parse(value);
|
|
412
|
+
if (!Number.isFinite(parsed))
|
|
413
|
+
throw new ToolInputError(`${field}: not a valid date-time`);
|
|
414
|
+
return { dateTime: new Date(parsed).toISOString().replace(/\.\d{3}Z$/, ""), timeZone: "UTC", allDay: false };
|
|
415
|
+
}
|
|
416
|
+
if (!timeZone)
|
|
417
|
+
throw new ToolInputError(`${field}: add a UTC offset (e.g. 2026-10-01T15:00:00-07:00) or pass timeZone`);
|
|
418
|
+
return { dateTime: value, timeZone, allDay: false };
|
|
419
|
+
}
|
|
420
|
+
function graphTimes(startValue, endValue, timeZone) {
|
|
421
|
+
const start = graphDateTime(startValue, timeZone, "start");
|
|
422
|
+
const end = graphDateTime(endValue, timeZone, "end");
|
|
423
|
+
if (start.allDay !== end.allDay)
|
|
424
|
+
throw new ToolInputError("start and end must both be dates or both be date-times");
|
|
425
|
+
return { start: { dateTime: start.dateTime, timeZone: start.timeZone }, end: { dateTime: end.dateTime, timeZone: end.timeZone }, isAllDay: start.allDay };
|
|
426
|
+
}
|
|
427
|
+
async function outlookCreateEvent(ctx, account, args) {
|
|
428
|
+
const attendees = addresses(args.attendees, "attendees");
|
|
429
|
+
const created = await graphSave(ctx, account, "POST", `${GRAPH}/me/events`, {
|
|
430
|
+
subject: args.summary,
|
|
431
|
+
...(args.description ? { body: { contentType: "Text", content: args.description } } : {}),
|
|
432
|
+
...graphTimes(args.start, args.end, args.timeZone),
|
|
433
|
+
...(args.location ? { location: { displayName: args.location } } : {}),
|
|
434
|
+
...(attendees.length ? { attendees: attendees.map((address) => ({ emailAddress: { address }, type: "required" })) } : {}),
|
|
435
|
+
});
|
|
436
|
+
return json({ account: account.email, created: graphEvent(created) });
|
|
437
|
+
}
|
|
438
|
+
const GRAPH_EVENT_FIELDS = "id,subject,start,end,isAllDay,location,organizer,attendees,webLink,bodyPreview,body,type,seriesMasterId,recurrence,originalStartTimeZone";
|
|
439
|
+
const UTC = 'outlook.timezone="UTC"';
|
|
440
|
+
const graphEventUrl = (id) => `${GRAPH}/me/events/${encodeURIComponent(id)}`;
|
|
441
|
+
async function graphGet(ctx, account, id, prefer = UTC) {
|
|
442
|
+
return parse(graphEventSchema, await call(ctx, account, `${graphEventUrl(id)}?${new URLSearchParams({ $select: GRAPH_EVENT_FIELDS })}`, { headers: { prefer }, expect: "json" }), "Outlook calendar");
|
|
443
|
+
}
|
|
444
|
+
async function graphSave(ctx, account, method, url, body) {
|
|
445
|
+
return parse(graphEventSchema, await call(ctx, account, url, {
|
|
446
|
+
method, headers: { "content-type": "application/json", prefer: UTC }, body: JSON.stringify(body), expect: "json",
|
|
447
|
+
}), "Outlook calendar");
|
|
448
|
+
}
|
|
449
|
+
/** Same as googleTarget: an occurrence or exception names its series in
|
|
450
|
+
* seriesMasterId. */
|
|
451
|
+
async function graphTarget(ctx, account, args) {
|
|
452
|
+
const scope = args.scope ?? "occurrence";
|
|
453
|
+
const event = await graphGet(ctx, account, args.id);
|
|
454
|
+
if (scope === "occurrence" && event.type === "seriesMaster")
|
|
455
|
+
throw new ToolInputError(`${args.id} is a whole series: pass the id of one occurrence (from outlook_calendar_list_events), or scope "series"`);
|
|
456
|
+
if (scope === "occurrence" || (!event.seriesMasterId && event.type !== "seriesMaster"))
|
|
457
|
+
return { scope: "occurrence", event, series: event };
|
|
458
|
+
const series = event.type === "seriesMaster" ? event : await graphGet(ctx, account, event.seriesMasterId);
|
|
459
|
+
const first = event.type === "seriesMaster" || millis(event.start?.dateTime ?? "") === millis(series.start?.dateTime ?? "");
|
|
460
|
+
return { scope: first ? "series" : scope, event, series };
|
|
461
|
+
}
|
|
462
|
+
/** Graph keeps each guest's reply by address, so only these two fields go back. */
|
|
463
|
+
const graphGuests = (event) => (event.attendees ?? []).map((guest) => ({ emailAddress: { address: guest.emailAddress?.address ?? "" }, type: guest.type ?? "required" }));
|
|
464
|
+
function graphFields(args, current) {
|
|
465
|
+
const guests = guestEdit(args);
|
|
466
|
+
return {
|
|
467
|
+
...(args.summary !== undefined ? { subject: args.summary } : {}),
|
|
468
|
+
...(args.description !== undefined ? { body: { contentType: "Text", content: args.description } } : {}),
|
|
469
|
+
...(args.location !== undefined ? { location: { displayName: args.location } } : {}),
|
|
470
|
+
...(guests ? {
|
|
471
|
+
attendees: editGuests(graphGuests(current), (guest) => guest.emailAddress.address, (guest) => guest.type === "resource", (address, room) => ({ emailAddress: { address }, type: room ? "resource" : "required" }), guests),
|
|
472
|
+
} : {}),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/** The local day an occurrence falls on, in its series' own time zone. */
|
|
476
|
+
async function seriesDay(ctx, account, event, series) {
|
|
477
|
+
const local = await graphGet(ctx, account, event.id, `outlook.timezone="${series.originalStartTimeZone ?? "UTC"}"`);
|
|
478
|
+
return { local, day: (local.start?.dateTime ?? "").slice(0, 10) };
|
|
479
|
+
}
|
|
480
|
+
function endSeriesBefore(series, day) {
|
|
481
|
+
const recurrence = series.recurrence;
|
|
482
|
+
return { recurrence: { pattern: recurrence.pattern, range: { ...recurrence.range, type: "endDate", endDate: dayBefore(day) } } };
|
|
483
|
+
}
|
|
484
|
+
async function outlookGetEvent(ctx, account, args) {
|
|
485
|
+
const event = await graphGet(ctx, account, args.id, `${UTC}, outlook.body-content-type="text"`);
|
|
486
|
+
return json({ account: account.email, timeZone: "UTC", event: { ...graphEvent(event), description: event.body?.content ? bounded(event.body.content, 2_000) : undefined } });
|
|
487
|
+
}
|
|
488
|
+
async function outlookUpdateEvent(ctx, account, args) {
|
|
489
|
+
const { scope, event, series } = await graphTarget(ctx, account, args);
|
|
490
|
+
const fields = graphFields(args, series);
|
|
491
|
+
needsChange(args, fields);
|
|
492
|
+
if (scope === "occurrence") {
|
|
493
|
+
const updated = await graphSave(ctx, account, "PATCH", graphEventUrl(event.id), { ...fields, ...(args.start !== undefined ? graphTimes(args.start, args.end, args.timeZone) : {}) });
|
|
494
|
+
return json({ account: account.email, scope, updated: graphEvent(updated) });
|
|
495
|
+
}
|
|
496
|
+
const zone = series.originalStartTimeZone ?? "UTC";
|
|
497
|
+
if (scope === "series") {
|
|
498
|
+
let times = {};
|
|
499
|
+
if (args.start !== undefined) {
|
|
500
|
+
// Shift the series' first start in its own zone, so it keeps its wall-clock time across DST.
|
|
501
|
+
const local = await graphGet(ctx, account, series.id, `outlook.timezone="${zone}"`);
|
|
502
|
+
const [start, end] = shiftSeries(local.start?.dateTime ?? "", event.start?.dateTime ?? "", event.isAllDay === true, args.start, args.end);
|
|
503
|
+
const at = (ms) => ({ dateTime: new Date(ms).toISOString().slice(0, 19), timeZone: zone });
|
|
504
|
+
times = { start: at(start), end: at(end) };
|
|
505
|
+
}
|
|
506
|
+
const updated = await graphSave(ctx, account, "PATCH", graphEventUrl(series.id), { ...fields, ...times });
|
|
507
|
+
return json({ account: account.email, scope, updated: graphEvent(updated) });
|
|
508
|
+
}
|
|
509
|
+
// Graph has no "this and following" edit either: same split as Google.
|
|
510
|
+
if (!series.recurrence || series.recurrence.range.type === "numbered") {
|
|
511
|
+
throw new ToolInputError("This series ends after a set number of times, so it cannot be split: change one occurrence or the whole series");
|
|
512
|
+
}
|
|
513
|
+
const { local, day } = await seriesDay(ctx, account, event, series);
|
|
514
|
+
const times = args.start !== undefined
|
|
515
|
+
? graphTimes(args.start, args.end, args.timeZone)
|
|
516
|
+
: { start: { dateTime: local.start?.dateTime, timeZone: zone }, end: { dateTime: local.end?.dateTime, timeZone: zone }, isAllDay: series.isAllDay === true };
|
|
517
|
+
const created = await graphSave(ctx, account, "POST", `${GRAPH}/me/events`, {
|
|
518
|
+
subject: series.subject,
|
|
519
|
+
...(series.body?.content ? { body: series.body } : {}),
|
|
520
|
+
...(series.location?.displayName ? { location: { displayName: series.location.displayName } } : {}),
|
|
521
|
+
attendees: graphGuests(series),
|
|
522
|
+
...fields,
|
|
523
|
+
...times,
|
|
524
|
+
recurrence: { pattern: series.recurrence.pattern, range: { ...series.recurrence.range, startDate: day } },
|
|
525
|
+
});
|
|
526
|
+
await graphSave(ctx, account, "PATCH", graphEventUrl(series.id), endSeriesBefore(series, day));
|
|
527
|
+
return json({ account: account.email, scope, updated: graphEvent(created), endedSeries: series.id, note: "The old series now ends before this occurrence; a new series with the changes starts here." });
|
|
528
|
+
}
|
|
529
|
+
async function outlookDeleteEvent(ctx, account, args) {
|
|
530
|
+
const { scope, event, series } = await graphTarget(ctx, account, args);
|
|
531
|
+
if (scope === "following") {
|
|
532
|
+
const { day } = await seriesDay(ctx, account, event, series);
|
|
533
|
+
await graphSave(ctx, account, "PATCH", graphEventUrl(series.id), endSeriesBefore(series, day));
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
await call(ctx, account, graphEventUrl((scope === "series" ? series : event).id), { method: "DELETE", expect: "none" });
|
|
537
|
+
}
|
|
538
|
+
return json({ account: account.email, cancelled: true, scope, id: scope === "occurrence" ? event.id : series.id });
|
|
539
|
+
}
|
|
540
|
+
export const CALENDAR_RUNNERS = {
|
|
541
|
+
calendar_list_events: calendarListEvents,
|
|
542
|
+
calendar_create_event: calendarCreateEvent,
|
|
543
|
+
calendar_get_event: calendarGetEvent,
|
|
544
|
+
calendar_update_event: calendarUpdateEvent,
|
|
545
|
+
calendar_delete_event: calendarDeleteEvent,
|
|
546
|
+
outlook_calendar_list_events: outlookListEvents,
|
|
547
|
+
outlook_calendar_create_event: outlookCreateEvent,
|
|
548
|
+
outlook_calendar_get_event: outlookGetEvent,
|
|
549
|
+
outlook_calendar_update_event: outlookUpdateEvent,
|
|
550
|
+
outlook_calendar_delete_event: outlookDeleteEvent,
|
|
551
|
+
};
|
|
@@ -89,7 +89,8 @@ export class PawService {
|
|
|
89
89
|
this.store.setBaseline(path, readText(this.root, path) ?? "");
|
|
90
90
|
this.state = { ...this.state, template: "chibitek" };
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
// an About me already there (written by hand, or filled from Nexus) is kept
|
|
93
|
+
const result = applyInterview(this.root, { ...answers, name: answers.name || this.options.person().name }, { personEmail: this.options.person().email, keepAboutMe: Boolean(this.aboutMe().trim()) });
|
|
93
94
|
this.saveState({ ...this.state, departments: result.departments, interviewedAt: Date.now() });
|
|
94
95
|
return this.status();
|
|
95
96
|
}
|
|
@@ -187,7 +187,8 @@ export function applyInterview(root, answers, options = {}) {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
const aboutMe = aboutMeMarkdown(answers, department?.name ?? "", options.now);
|
|
190
|
-
|
|
190
|
+
if (!options.keepAboutMe)
|
|
191
|
+
writeText(root, "me/about-me.md", aboutMe);
|
|
191
192
|
applyLocks(root, manifest, options.personEmail ?? "");
|
|
192
193
|
return { departments, aboutMe };
|
|
193
194
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PASS } from "../routes/table.js";
|
|
2
|
+
import { VaultCsvError, vaultLoginsFromCsv } from "../../shared/vault-csv.js";
|
|
2
3
|
import { VaultError } from "./store.js";
|
|
3
4
|
/** The owner is supplied by the authenticated host, never from request JSON.
|
|
4
5
|
* Authorization runs on reads too, and is checked again after reading a body. */
|
|
@@ -13,6 +14,25 @@ export function createVaultRoutes(deps) {
|
|
|
13
14
|
const store = deps.store();
|
|
14
15
|
if (!store)
|
|
15
16
|
return json(res, 503, { error: "Vault encryption is unavailable. Unlock the secure store and try again." });
|
|
17
|
+
if (path === "/api/vault/import") {
|
|
18
|
+
if (method !== "POST")
|
|
19
|
+
return json(res, 405, { error: "Method not allowed." });
|
|
20
|
+
const body = await readBody(req, 8_000_000).catch(() => null);
|
|
21
|
+
if (deps.owner(req, auth) !== ownerId || deps.store() !== store)
|
|
22
|
+
return json(res, 403, { error: "Vault access changed. Try again." });
|
|
23
|
+
if (typeof body?.csv !== "string")
|
|
24
|
+
return json(res, 400, { error: "Choose a CSV file exported from your password manager." });
|
|
25
|
+
try {
|
|
26
|
+
const { logins, skipped } = vaultLoginsFromCsv(body.csv);
|
|
27
|
+
return json(res, 200, { ...store.importLogins(ownerId, logins), skipped });
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// Neither message echoes anything from a row.
|
|
31
|
+
if (error instanceof VaultError || error instanceof VaultCsvError)
|
|
32
|
+
return json(res, 400, { error: error.message });
|
|
33
|
+
return json(res, 503, { error: "Vault could not complete the request." });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
16
36
|
const item = /^\/api\/vault\/([0-9a-f-]{36})$/.exec(path);
|
|
17
37
|
if (path !== "/api/vault" && !item)
|
|
18
38
|
return json(res, 404, { error: "Vault item is unavailable." });
|
|
@@ -143,6 +143,30 @@ export class VaultStore {
|
|
|
143
143
|
return vaultMetadata(item);
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
|
+
/** Adds logins in one write. A login already saved for the same website
|
|
147
|
+
* and username is left as it is and counted in `duplicates`. */
|
|
148
|
+
importLogins(ownerId, logins) {
|
|
149
|
+
owner(ownerId);
|
|
150
|
+
const parsed = logins.map((input) => vaultInputSchema.safeParse(input));
|
|
151
|
+
if (parsed.some((result) => !result.success))
|
|
152
|
+
throw new VaultError("Invalid Vault item.");
|
|
153
|
+
return this.mutate((items) => {
|
|
154
|
+
const key = (item) => item.kind === "login" ? `${item.website}\n${item.username}` : "";
|
|
155
|
+
const seen = new Set(items.filter((item) => item.ownerId === ownerId).map(key));
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
let added = 0;
|
|
158
|
+
for (const { data } of parsed) {
|
|
159
|
+
if (seen.has(key(data)))
|
|
160
|
+
continue;
|
|
161
|
+
seen.add(key(data));
|
|
162
|
+
items.push({ ...data, id: randomUUID(), ownerId, createdAt: now, updatedAt: now });
|
|
163
|
+
added++;
|
|
164
|
+
}
|
|
165
|
+
if (items.length > MAX_ITEMS)
|
|
166
|
+
throw new VaultError(`Vault holds up to ${MAX_ITEMS} items. This import would go over that.`);
|
|
167
|
+
return { added, duplicates: logins.length - added };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
146
170
|
update(ownerId, id, input) {
|
|
147
171
|
owner(ownerId);
|
|
148
172
|
if (!input || typeof input !== "object" || Array.isArray(input))
|