@awesomate/hosting-mcp 0.68.0 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +107 -2
- package/package.json +1 -1
- package/skill/CHANGELOG.json +12 -0
- package/skill/awesomate-bookings/SKILL.md +83 -0
package/dist/index.js
CHANGED
|
@@ -39861,6 +39861,7 @@ var TOOL_FEATURE = {
|
|
|
39861
39861
|
awesomate_crm_queries: "contacts",
|
|
39862
39862
|
awesomate_crm_recipes: "contacts",
|
|
39863
39863
|
awesomate_crm_email: "email",
|
|
39864
|
+
awesomate_bookings: "bookings",
|
|
39864
39865
|
awesomate_knowledge_status: "knowledge",
|
|
39865
39866
|
awesomate_knowledge_provision: "knowledge",
|
|
39866
39867
|
awesomate_knowledge_sources: "knowledge",
|
|
@@ -40320,7 +40321,7 @@ async function filesAction(config3, args) {
|
|
|
40320
40321
|
const out = await hubPost(config3, "/api/files/move", { from, to: swapTopFolder(from, "private") });
|
|
40321
40322
|
return {
|
|
40322
40323
|
...out,
|
|
40323
|
-
note: `Moved from ${from}. Its public address
|
|
40324
|
+
note: `Moved from ${from}. Its public address stops serving it within seconds; a browser that already opened it may show its own copy for a while, and a copy someone downloaded is not recalled. Any workflow, page or app that used it needs changing.`
|
|
40324
40325
|
};
|
|
40325
40326
|
}
|
|
40326
40327
|
case "delete": {
|
|
@@ -41578,6 +41579,7 @@ var TITLES = {
|
|
|
41578
41579
|
awesomate_crm_assistant: "Set up the AI in your app's conversations",
|
|
41579
41580
|
awesomate_crm_notifications: "Email people when someone replies in your app",
|
|
41580
41581
|
awesomate_crm_hooks: "Send record changes to your n8n",
|
|
41582
|
+
awesomate_bookings: "Your booking diary and booking box",
|
|
41581
41583
|
awesomate_crm_email: "Draft an email to a contact list"
|
|
41582
41584
|
};
|
|
41583
41585
|
function annotate(name, kind) {
|
|
@@ -42988,7 +42990,7 @@ server.registerTool(
|
|
|
42988
42990
|
"awesomate_files",
|
|
42989
42991
|
{
|
|
42990
42992
|
annotations: annotate("awesomate_files", DESTRUCTIVE),
|
|
42991
|
-
description: `The account's Files: the folders on their automation account (n8n) that workflows read and write, and that the hub shows at Knowledge, Files. Three top folders: public/ (every file has a public_url anyone can open with no login: images for a site or email, downloads, media a workflow made), private/ (only the account, its workflows and the hub) and temp/ (scratch space). Support Plus, Pro and Embedded; the owner must also have switched on "Open your automation account's file folders" in Settings, Privacy. Paths are relative to the files folder and start with the top folder, e.g. public/images/logo.png. action 'status': whether Files is on, why not, and storage used against the plan's limit: call this first. 'list' {path?}: one folder (no path = the top folders); files in public/ carry public_url. 'search' {q?, top?, exts?}: find files by name across the folders (first 200 matches; truncated:true means narrow it). 'download' {path, to?, overwrite?}: save a file to THIS computer (to = a local file or folder, default the working directory); never overwrites without overwrite:true. 'upload' {localPath, path, overwrite?}: send a file from this computer (max 50 MB each); path is the target file, or a folder ending in / to keep the name. Upload into public/ only when the user wants it publicly reachable. 'mkdir' {path}. 'move' {path, to}: rename or move; moving into public/ needs confirm:true. 'make_public' {path, confirm:true}: moves private/ or temp/ to the same place under public/ and returns its public_url: ASK the user first, anyone with the address can read it. 'make_private' {path}: moves it out of public
|
|
42993
|
+
description: `The account's Files: the folders on their automation account (n8n) that workflows read and write, and that the hub shows at Knowledge, Files. Three top folders: public/ (every file has a public_url anyone can open with no login: images for a site or email, downloads, media a workflow made), private/ (only the account, its workflows and the hub) and temp/ (scratch space). Support Plus, Pro and Embedded; the owner must also have switched on "Open your automation account's file folders" in Settings, Privacy. Paths are relative to the files folder and start with the top folder, e.g. public/images/logo.png. action 'status': whether Files is on, why not, and storage used against the plan's limit: call this first. 'list' {path?}: one folder (no path = the top folders); files in public/ carry public_url. 'search' {q?, top?, exts?}: find files by name across the folders (first 200 matches; truncated:true means narrow it). 'download' {path, to?, overwrite?}: save a file to THIS computer (to = a local file or folder, default the working directory); never overwrites without overwrite:true. 'upload' {localPath, path, overwrite?}: send a file from this computer (max 50 MB each); path is the target file, or a folder ending in / to keep the name. Upload into public/ only when the user wants it publicly reachable. 'mkdir' {path}. 'move' {path, to}: rename or move; moving into public/ needs confirm:true. 'make_public' {path, confirm:true}: moves private/ or temp/ to the same place under public/ and returns its public_url: ASK the user first, anyone with the address can read it. 'make_private' {path}: moves it out of public/; its public address stops serving it within seconds (the hub clears Cloudflare's copy, and does the same after a delete or a replace), though a browser that already opened it may show its own copy until it expires, and a downloaded copy is not recalled. 'delete' {path, confirm:true}: moves to the trash (not erased); ask first. Moving or renaming changes the path, so tell the user any workflow, page or app using the old path or address needs the new one. Contents stream between this computer and the hub: never paste a file's contents into the conversation to move it. A 507 files_storage_full means the plan's Files allowance is used: say how much, offer to find large or old files to remove, and leave any plan change to the user.`,
|
|
42992
42994
|
inputSchema: {
|
|
42993
42995
|
action: external_exports.enum(FILES_ACTIONS),
|
|
42994
42996
|
path: external_exports.string().max(1024).optional().describe("A path in Files, starting public/, private/ or temp/. For upload, the target (a folder ending in / keeps the local name)."),
|
|
@@ -43436,6 +43438,109 @@ server.registerTool(
|
|
|
43436
43438
|
}
|
|
43437
43439
|
}
|
|
43438
43440
|
);
|
|
43441
|
+
server.registerTool(
|
|
43442
|
+
"awesomate_bookings",
|
|
43443
|
+
{
|
|
43444
|
+
annotations: annotate("awesomate_bookings", MUTATING),
|
|
43445
|
+
description: `The business's own booking diary: customers book appointments or classes on the business's website, get an email with an invite and a link to change or cancel, and each booking lands in Contacts linked to the person. Read the awesomate-bookings skill first. Reading is every plan; every action that changes something needs Support Plus or above (on Essentials the hub answers upgrade_required: tell the owner they can do it in the hub under Contacts, Bookings, and do not retry). action 'setup': calendars, services, and this month's website bookings against the plan's limit. 'save_calendar' {key, name, timezone, hours, min_notice_minutes?, buffer_minutes?, max_per_day?, notify_email?, active?}: a person, room or resource with weekly hours ({"mon": [["09:00","17:00"]]}, 24-hour, in its own zone); a change names only what moves. 'save_service' {key, name, minutes, calendars, capacity?, price_text?, location?, cancel_cutoff_hours?, intake?, description?, active?}: capacity above 1 is a class several people join at one start; price is shown, never charged. 'open_times' {service, from?, to?, calendar?}. 'list' {from?, to?, status?}: bookings with each customer and their answers. 'book' {service, calendar, starts_at (from open_times), email, first_name?, last_name?, phone?, answers?, notify?, outside_hours?}. 'move' {booking_id, starts_at, notify?, outside_hours?}. 'cancel' {booking_id, reason?, notify?}. notify: false skips the customer's email; the calendar's notice email always hears. 'keys': the booking keys for the account's websites, each with the snippet to paste. 'create_key' {website}: the booking box for one site (https), returns the snippet. 'update_key' {key_id, active?, website?}. Nobody can double-book a time: the hub re-checks under a lock and answers 409 with why (not_open, slot_taken, session_full, day_full).`,
|
|
43446
|
+
inputSchema: {
|
|
43447
|
+
action: external_exports.enum(["setup", "save_calendar", "save_service", "open_times", "list", "book", "move", "cancel", "keys", "create_key", "update_key"]),
|
|
43448
|
+
key: external_exports.string().max(41).optional().describe("save_calendar, save_service: lower case, digits and underscores"),
|
|
43449
|
+
name: external_exports.string().max(80).optional(),
|
|
43450
|
+
timezone: external_exports.string().max(60).optional().describe("IANA zone, like Australia/Sydney"),
|
|
43451
|
+
hours: external_exports.record(external_exports.string(), external_exports.array(external_exports.tuple([external_exports.string(), external_exports.string()]))).optional().describe('save_calendar: {"mon": [["09:00","17:00"]], ...}'),
|
|
43452
|
+
min_notice_minutes: external_exports.number().int().min(0).optional(),
|
|
43453
|
+
buffer_minutes: external_exports.number().int().min(0).optional(),
|
|
43454
|
+
max_per_day: external_exports.number().int().min(1).nullable().optional(),
|
|
43455
|
+
notify_email: external_exports.string().max(254).nullable().optional(),
|
|
43456
|
+
active: external_exports.boolean().optional(),
|
|
43457
|
+
minutes: external_exports.number().int().optional().describe("save_service: length in minutes"),
|
|
43458
|
+
capacity: external_exports.number().int().min(1).optional(),
|
|
43459
|
+
price_text: external_exports.string().max(80).optional(),
|
|
43460
|
+
location: external_exports.string().max(500).optional(),
|
|
43461
|
+
description: external_exports.string().max(2e3).optional(),
|
|
43462
|
+
cancel_cutoff_hours: external_exports.number().int().min(0).optional(),
|
|
43463
|
+
intake: external_exports.array(external_exports.object({
|
|
43464
|
+
key: external_exports.string().max(41),
|
|
43465
|
+
label: external_exports.string().max(200),
|
|
43466
|
+
type: external_exports.enum(["text", "textarea", "select", "multiselect", "url"]),
|
|
43467
|
+
required: external_exports.boolean().optional(),
|
|
43468
|
+
options: external_exports.array(external_exports.string().max(100)).max(50).optional()
|
|
43469
|
+
})).max(20).optional().describe("save_service: the questions asked when booking"),
|
|
43470
|
+
calendars: external_exports.array(external_exports.string().max(41)).max(50).optional().describe("save_service: calendar keys that offer it"),
|
|
43471
|
+
service: external_exports.string().max(41).optional(),
|
|
43472
|
+
calendar: external_exports.string().max(41).optional(),
|
|
43473
|
+
from: external_exports.string().max(40).optional(),
|
|
43474
|
+
to: external_exports.string().max(40).optional(),
|
|
43475
|
+
status: external_exports.enum(["confirmed", "cancelled", "completed", "no_show"]).optional(),
|
|
43476
|
+
starts_at: external_exports.string().max(40).optional().describe("an ISO time from open_times"),
|
|
43477
|
+
email: external_exports.string().max(254).optional(),
|
|
43478
|
+
first_name: external_exports.string().max(80).optional(),
|
|
43479
|
+
last_name: external_exports.string().max(80).optional(),
|
|
43480
|
+
phone: external_exports.string().max(40).optional(),
|
|
43481
|
+
answers: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.array(external_exports.string())])).optional(),
|
|
43482
|
+
notify: external_exports.boolean().optional(),
|
|
43483
|
+
outside_hours: external_exports.boolean().optional(),
|
|
43484
|
+
booking_id: external_exports.string().max(40).optional(),
|
|
43485
|
+
reason: external_exports.string().max(500).optional(),
|
|
43486
|
+
website: external_exports.string().max(300).optional().describe("create_key, update_key: the site address, like https://example.com.au"),
|
|
43487
|
+
key_id: external_exports.number().int().optional().describe("update_key: from keys")
|
|
43488
|
+
}
|
|
43489
|
+
},
|
|
43490
|
+
async (a) => {
|
|
43491
|
+
try {
|
|
43492
|
+
const cfg = requireConfig();
|
|
43493
|
+
const base = "/api/my-crm/v1/bookings";
|
|
43494
|
+
const pick2 = (...ks) => Object.fromEntries(ks.map((k) => [k, a[k]]).filter(([, v]) => v !== void 0));
|
|
43495
|
+
const qs = (o) => Object.entries(o).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
|
|
43496
|
+
const origin = (w) => new URL(w).origin;
|
|
43497
|
+
switch (a.action) {
|
|
43498
|
+
case "setup":
|
|
43499
|
+
return textResult(await hubGet(cfg, `${base}/setup`));
|
|
43500
|
+
case "save_calendar":
|
|
43501
|
+
if (!a.key) return errorResult(new Error("save_calendar needs key."));
|
|
43502
|
+
return textResult(await hubPut(
|
|
43503
|
+
cfg,
|
|
43504
|
+
`${base}/calendars/${encodeURIComponent(a.key)}`,
|
|
43505
|
+
pick2("name", "timezone", "hours", "min_notice_minutes", "buffer_minutes", "max_per_day", "notify_email", "active")
|
|
43506
|
+
));
|
|
43507
|
+
case "save_service":
|
|
43508
|
+
if (!a.key) return errorResult(new Error("save_service needs key."));
|
|
43509
|
+
return textResult(await hubPut(
|
|
43510
|
+
cfg,
|
|
43511
|
+
`${base}/services/${encodeURIComponent(a.key)}`,
|
|
43512
|
+
pick2("name", "minutes", "capacity", "price_text", "location", "description", "cancel_cutoff_hours", "intake", "calendars", "active")
|
|
43513
|
+
));
|
|
43514
|
+
case "open_times":
|
|
43515
|
+
if (!a.service) return errorResult(new Error("open_times needs service."));
|
|
43516
|
+
return textResult(await hubGet(cfg, `${base}/open-times?${qs({ service: a.service, from: a.from, to: a.to, calendar: a.calendar })}`));
|
|
43517
|
+
case "list":
|
|
43518
|
+
return textResult(await hubGet(cfg, `${base}/?${qs({ from: a.from, to: a.to, status: a.status })}`));
|
|
43519
|
+
case "book":
|
|
43520
|
+
return textResult(await hubPost(cfg, base, pick2("service", "calendar", "starts_at", "email", "first_name", "last_name", "phone", "answers", "notify", "outside_hours")));
|
|
43521
|
+
case "move":
|
|
43522
|
+
if (!a.booking_id) return errorResult(new Error("move needs booking_id, from list."));
|
|
43523
|
+
return textResult(await hubPost(cfg, `${base}/${encodeURIComponent(a.booking_id)}/move`, pick2("starts_at", "notify", "outside_hours")));
|
|
43524
|
+
case "cancel":
|
|
43525
|
+
if (!a.booking_id) return errorResult(new Error("cancel needs booking_id, from list."));
|
|
43526
|
+
return textResult(await hubPost(cfg, `${base}/${encodeURIComponent(a.booking_id)}/cancel`, pick2("reason", "notify")));
|
|
43527
|
+
case "keys":
|
|
43528
|
+
return textResult(await hubGet(cfg, `${base}/keys`));
|
|
43529
|
+
case "create_key":
|
|
43530
|
+
if (!a.website) return errorResult(new Error("create_key needs website: the site's address, like https://example.com.au."));
|
|
43531
|
+
return textResult(await hubPost(cfg, `${base}/keys`, { name: new URL(a.website).hostname, allowed_origins: [origin(a.website)] }));
|
|
43532
|
+
case "update_key":
|
|
43533
|
+
if (a.key_id === void 0) return errorResult(new Error("update_key needs key_id, from keys."));
|
|
43534
|
+
return textResult(await hubPatch(cfg, `${base}/keys/${a.key_id}`, {
|
|
43535
|
+
...a.active === void 0 ? {} : { active: a.active },
|
|
43536
|
+
...a.website ? { allowed_origins: [origin(a.website)] } : {}
|
|
43537
|
+
}));
|
|
43538
|
+
}
|
|
43539
|
+
} catch (err) {
|
|
43540
|
+
return errorResult(err);
|
|
43541
|
+
}
|
|
43542
|
+
}
|
|
43543
|
+
);
|
|
43439
43544
|
server.registerTool(
|
|
43440
43545
|
"awesomate_crm_notifications",
|
|
43441
43546
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awesomate/hosting-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.0",
|
|
4
4
|
"description": "Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, n8n automations, and build Node/static apps + databases",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
package/skill/CHANGELOG.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"versions": [
|
|
3
|
+
{
|
|
4
|
+
"version": "0.70.0",
|
|
5
|
+
"highlights": [
|
|
6
|
+
"A file you make private, delete or replace in your Files now stops showing at its public address within seconds, instead of up to 4 hours"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.69.0",
|
|
11
|
+
"highlights": [
|
|
12
|
+
"Bookings: your customers book appointments or classes on your own website, get an email with an invite and a link to change or cancel, and each booking lands in Contacts. Claude sets up your calendars, services and the booking box (awesomate_bookings), and can book, move and cancel for you"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
3
15
|
{
|
|
4
16
|
"version": "0.68.0",
|
|
5
17
|
"highlights": [
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: awesomate-bookings
|
|
3
|
+
description: Let the business's own customers book appointments or classes online, and manage those bookings. Sets up calendars (a person, room or resource with weekly hours), services (what is booked, how long, how many places, the questions asked), and the booking box for their website, then books, moves and cancels for them. Use when the user says "booking", "bookings", "appointments", "book online", "booking calendar", "scheduling", "let customers book", "Calendly", "class bookings", "who's booked tomorrow", or wants a booking page on their site. Companion to awesomate-app-builder and awesomate-hosting (the page it goes on) and awesomate-n8n (follow-ups): same connection, same token.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Awesomate Bookings: customers book themselves, bookings land in Contacts
|
|
7
|
+
|
|
8
|
+
A business's customers pick a time on the business's own website, get an email with a calendar
|
|
9
|
+
invite and a link to change or cancel, and the business gets a notice for each booking. Every
|
|
10
|
+
booking is a record in the business's own Contacts, linked to the person who made it.
|
|
11
|
+
|
|
12
|
+
Everything goes through one tool, `awesomate_bookings`. Work in plain words with the owner: ask
|
|
13
|
+
what people book, who with, and when they are open, then set it up and show them the result in a
|
|
14
|
+
sentence or two.
|
|
15
|
+
|
|
16
|
+
## What it is made of
|
|
17
|
+
|
|
18
|
+
- **Calendars**: a person, room or resource people book. Each has weekly hours in its own time
|
|
19
|
+
zone, how much notice it needs, a gap between bookings, an optional daily limit, and the email
|
|
20
|
+
that hears about every booking (with an invite for their own calendar). Set the notice email:
|
|
21
|
+
without one, customers' replies to booking emails reach nobody.
|
|
22
|
+
- **Services**: what is booked. A length in minutes, places (1 for one person at a time, more for
|
|
23
|
+
a class several people join), a price shown as text (nothing is charged), where it happens,
|
|
24
|
+
questions to ask when booking, how many hours before the start changes close online, and which
|
|
25
|
+
calendars offer it.
|
|
26
|
+
- **The booking box**: one key per website, locked to that site's address. `create_key` returns the
|
|
27
|
+
snippet to paste.
|
|
28
|
+
|
|
29
|
+
## Setting up (in this order)
|
|
30
|
+
|
|
31
|
+
1. `awesomate_bookings` action `save_calendar` with `key` (lower case, like `nicole`), `name`,
|
|
32
|
+
`timezone` (like `Australia/Sydney`), `hours` (`{"mon": [["09:00","17:00"]], ...}`, 24-hour, no
|
|
33
|
+
overlaps), and `notify_email`.
|
|
34
|
+
2. `save_service` with `key`, `name`, `minutes`, `calendars` (keys), and optionally `capacity`,
|
|
35
|
+
`price_text`, `location`, `cancel_cutoff_hours`, `intake` (questions:
|
|
36
|
+
`[{"key":"reason","label":"What brings you in?","type":"textarea","required":true}]`; types are
|
|
37
|
+
text, textarea, select, multiselect, url; select and multiselect take `options`).
|
|
38
|
+
3. `open_times` with `service` to check the times look right before telling the owner it is ready.
|
|
39
|
+
4. `create_key` with `website` (the site's address, https). Put the returned `snippet` on the page
|
|
40
|
+
the owner wants, or give it to them to paste:
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<div data-awesomate-bookings></div>
|
|
44
|
+
<script src="https://hub.awesomate.ai/widget/v1/bookings.js" data-key="bk_..." async></script>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
On the div, `data-service="<key>"` offers one service only, and `data-accent="#0f766e"` colours the
|
|
48
|
+
buttons to match the site. On a WordPress site, a Custom HTML block holds both lines. On a site you
|
|
49
|
+
build with awesomate-app-builder, put them in the page where booking belongs (a "Book now" section,
|
|
50
|
+
or a /book page) and deploy as usual. The box only works on the website named on its key: to use it
|
|
51
|
+
on another domain, `update_key` with `allowed_origins`.
|
|
52
|
+
|
|
53
|
+
Changing a calendar or service later: call the same `save_` action with only what changes.
|
|
54
|
+
|
|
55
|
+
## Running the diary
|
|
56
|
+
|
|
57
|
+
- `list` (optionally `from`, `to`, `status`): upcoming bookings with each customer's name, email,
|
|
58
|
+
phone and answers. Use it for "who's booked tomorrow".
|
|
59
|
+
- `book`: book someone in (`service`, `calendar`, `starts_at` from `open_times`, the customer's
|
|
60
|
+
`email`, `first_name`, `last_name`, `phone`, `answers`). `notify: false` when the customer should
|
|
61
|
+
not get an email (a booking taken by phone that they already know about). Staff may book outside
|
|
62
|
+
the hours with `outside_hours: true`; nobody can double-book a time.
|
|
63
|
+
- `move` (`booking_id`, `starts_at`) and `cancel` (`booking_id`, `reason`): the customer is emailed
|
|
64
|
+
unless `notify: false`, and the calendar's notice email always is.
|
|
65
|
+
|
|
66
|
+
## Follow-ups through n8n (Pro)
|
|
67
|
+
|
|
68
|
+
Each booking is a record of the `booking` kind in Contacts. On Pro, an account with one of its own
|
|
69
|
+
apps (`awesomate_crm_apps`) can add a hook with `awesomate_crm_hooks` and `kinds: ["booking"]` to
|
|
70
|
+
send each booking's created, updated and archived events to its own n8n: an SMS reminder, copying it
|
|
71
|
+
into Google Calendar with the owner's own n8n credential, a thank-you after the visit. Bookings
|
|
72
|
+
themselves cannot be written through `awesomate_crm_write`; only these actions change them.
|
|
73
|
+
|
|
74
|
+
## Plans and limits
|
|
75
|
+
|
|
76
|
+
- Every plan can set up bookings and take them on its website. Essentials takes up to 50 website
|
|
77
|
+
bookings a month; bookings the business makes itself never count.
|
|
78
|
+
- Changing bookings from Claude Code (the write actions here) needs Support Plus or above. On
|
|
79
|
+
Essentials, the tool answers `upgrade_required`: say the owner can do the same in the hub under
|
|
80
|
+
Contacts, Bookings, and do not retry.
|
|
81
|
+
- Payments and deposits are not part of bookings yet. Do not promise them; the price is shown only.
|
|
82
|
+
- Two-way sync with the owner's own Google or Outlook calendar is not available yet: the notice
|
|
83
|
+
email's invite is how a booking reaches their calendar. Do not promise automatic sync.
|