@chibitek/chibi 0.1.172 → 0.1.174
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-CGoW52MW.js → abnfDiagram-O67JEVCF-BVDndX1g.js} +1 -1
- package/dist/assets/{arc-DEkKLw4F.js → arc-WduS6wYB.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-Cyi6MYs6.js → architectureDiagram-NJMV4G6O-CYK9LkCf.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CddA0wa_.js → blockDiagram-BEXU5L5S-BMj-aPW9.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Dg8q9n90.js → c4Diagram-YGBWAQC7-cXYIStgL.js} +1 -1
- package/dist/assets/channel-TratrwmM.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BSMub34c.js → chunk-2Q5K7J3B-CZ_nCppA.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW--Xc7Up5y.js → chunk-3FUC2YCW-C3wRAbtM.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-B9Ae4f30.js → chunk-5VM5RSS4-CMvgOXJP.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dh1To9G3.js → chunk-GTNCS2PH-9wBuLQzU.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-C_5eiV2n.js → chunk-GWA4HPMP-BpJMmfAi.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-5wQuPsks.js → chunk-JWPE2WC7-BnM06ABv.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BGXS05ur.js → chunk-NETBCI7D-CjjiiXiT.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-ClIMhmD5.js → chunk-WEXAMYUT-D3heHHZs.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CcKs11-L.js → chunk-XXDRQBXY-n_P41ZlM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BCiLckGU.js → classDiagram-v2-NBCMYWYE-Dz60Ek-6.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-JyAqT-ND.js → cose-bilkent-JH36ORCC-BCt6vBi9.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BznovZj0.js → cynefin-OW5HDTMX-U0uq4EAQ.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BDRrTIEB.js → cynefinDiagram-VND7K2PF-CaQX0eUJ.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BbWguhLj.js → dagre-6A5THRUB-DlCMESYH.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BMk1KTBr.js → diagram-22UHCM2B-Cl1xmlTD.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-ChnEP2in.js → diagram-3UASUU5V-Cus3CPg7.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-C78imnTV.js → diagram-ATOU4E4O-i06rMvae.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-Dekx02f_.js → diagram-CDSNMT55-CUM5_heN.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-D2__fPwe.js → diagram-MLGK6HIB-B5NKRruo.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-C9l7MZm7.js → diagram-MPIPVDR6-CAumAcGl.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D4x0h3XT.js → ebnfDiagram-ZINNZB2B-B19d7d8l.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-twChz5io.js → elk-276RUBZZ-NJEpfRWi.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BXEKWWMq.js → erDiagram-OPXOYQCR-BExk6CHc.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Ct-XrJMr.js → flowDiagram-KWPJA3E3-ClExQ_p0.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-Bn92lxCs.js → ganttDiagram-FUAMR5RP-1MeM9sSl.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dvfjo7cn.js → gitGraphDiagram-X574FWY7-Tw9Dudpa.js} +1 -1
- package/dist/assets/{index-DvneZE6Z.js → index-KYHldywn.js} +140 -140
- package/dist/assets/{index-CarzpTfN.js → index-qsorLr2x.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-MJ14OyNX.js → infoDiagram-VRGFBTTK-ChU2tjvl.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-nf9xhTNX.js → ishikawaDiagram-OU5B5YK6-BeTbrX2Q.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DqfK6X2e.js → journeyDiagram-ZHPQQLJL-B7ijaWBD.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-CjUR1QYT.js → kanban-definition-PNTS6WVX-JBmK2F1r.js} +1 -1
- package/dist/assets/{linear-dPqGsZwL.js → linear-B7FXe3Ue.js} +1 -1
- package/dist/assets/{mermaid.core-zL0DLPAI.js → mermaid.core-BPlfZpqv.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-C1-Jqg3_.js → mindmap-definition-NLK3R4M7-CmT3S5QR.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CFlvuAHC.js → pegDiagram-GJSIUBJH-CFFHIcdQ.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-BvyT0IgF.js → pieDiagram-5QR66LMP-yTiPmXRV.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-bg7JeR2c.js → quadrantDiagram-O4NWA36T-Cw8u7esm.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-dF2Nqim8.js → railroadDiagram-XR7U4H2S-BfKggsOW.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CwVu_Or6.js → requirementDiagram-PLB6GJNP-Cf3n79jF.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-wBC3-mRy.js → sankeyDiagram-IPEJSGJF-B2NkSZig.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-Bs9lHMf-.js → sequenceDiagram-PO4LG4MO-BfFYiYK3.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-d6YiGOIQ.js → sizeCapture-INFHLROL-CZ_2nC3f.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK--f6dscuE.js → stateDiagram-v2-GCMORJYK-B9jwzMXf.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-CQpzBoAc.js → swimlanes-2SLR337P-Bxhafu77.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-DWNGCaH3.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-DpoyUyqt.js → timeline-definition-EJHVYXUP-Dle5SJls.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-Dg7mR3GW.js → usecaseDiagram-POWQR4AR-CgRTu9EO.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CoEfTL7k.js → vennDiagram-UO4OBE2U-D7m_1z6B.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-24tIY01C.js → wardleyDiagram-VNRHLVJA-7-TzmX4L.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-CsoqIRqn.js → xychartDiagram-PMCCYNJV-BVXXdviS.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/index.js +2797 -1875
- package/dist-server/openmausbot.js +64 -2
- package/dist-server/pair-cli.js +64 -2
- package/dist-server/server/builtin-chibi.js +1 -1
- package/dist-server/server/dictation.js +8 -0
- package/dist-server/server/drivers/claude.js +4 -4
- package/dist-server/server/engine-tiers-view.js +49 -0
- package/dist-server/server/engine-tiers.js +37 -55
- package/dist-server/server/harness-import/engine-policy.js +6 -1
- package/dist-server/server/index.js +12 -13
- 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/personal-bot.js +0 -26
- package/dist-server/server/remote-desktop.js +139 -7
- package/dist-server/server/routes/engine-tiers.js +12 -1
- package/dist-server/server/subscription-bots.js +2 -21
- package/dist-server/server/turn-resources.js +28 -0
- package/dist-server/server/workspace-backup-archive.js +138 -0
- package/dist-server/server/workspace-backup-http.js +50 -8
- package/dist-server/server/workspace-backup-private.js +83 -0
- package/dist-server/server/workspace-backup-secrets.js +90 -0
- package/dist-server/server/workspace-backup.js +171 -235
- package/dist-server/server/workspace-restore-paths.js +186 -0
- package/dist-server/shared/oauth-providers.js +2 -1
- package/dist-server/shared/provider-accounts.js +29 -0
- package/package.json +1 -1
- package/dist/assets/channel-Dda8O6gd.js +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C1ko4o6t.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
|
+
};
|
|
@@ -1,29 +1,3 @@
|
|
|
1
|
-
// The Private Bot's rules (docs/chibitek/team-room-and-personal-bot.md,
|
|
2
|
-
// section 1; shared/personal-bot.ts). Pure, so every rule is unit tested
|
|
3
|
-
// without a server; server/index.ts wires them into each engine-selection
|
|
4
|
-
// point, the roster and the routes.
|
|
5
|
-
//
|
|
6
|
-
// 1. Personal accounts are optional; prefer them, then company compute.
|
|
7
|
-
// Company Mezcal requires explicit turn-scoped confirmation.
|
|
8
|
-
// 2. The Company / Personal tag on an AI account is a name for who owns
|
|
9
|
-
// it, not an access wall (Erick, 2026-10-24): company bots may run on
|
|
10
|
-
// Personal accounts too, and the Private Bot on any subscription.
|
|
11
|
-
// 3. Local models remain opt-in.
|
|
12
|
-
// 4. It has no company connections; company bots never see personal ones.
|
|
13
|
-
// 5. No delegation either way, and it is not on anyone's roster, except
|
|
14
|
-
// the link with Chibi (Erick, 2026-10-09): while the person's "Let my
|
|
15
|
-
// Private Bot and Chibi talk" is on (the default) and MDM
|
|
16
|
-
// PrivateBotChibiLink is not `disabled`, the Private Bot and Chibi (and
|
|
17
|
-
// only Chibi) may message each other and hand each other work. It
|
|
18
|
-
// orchestrates like Chibi, through Chibi: Chibi routes to company bots.
|
|
19
|
-
// 6. MDM `PersonalBot: disabled`: it can't be created, and an existing one
|
|
20
|
-
// is hidden, never deleted.
|
|
21
|
-
// 7. It never spawns bots, sets up teams or makes rooms
|
|
22
|
-
// (privateBotInternalRefusal), whatever its prompt or tools say.
|
|
23
|
-
//
|
|
24
|
-
// The owner tag is stored apart from the accounts themselves (config.json
|
|
25
|
-
// `personal.engineOwners`): tagging an account never touches how its
|
|
26
|
-
// credentials are stored.
|
|
27
1
|
import { readFileSync } from "node:fs";
|
|
28
2
|
import { readManagedPreferences, parseManagedPreferences } from "../electron/managed-prefs.mjs";
|
|
29
3
|
import { CHIEF_OF_STAFF_BUILTIN } from "../shared/chief-of-staff.js";
|