slotlock 0.8.0 → 0.9.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +237 -0
  2. package/agent-server.d.ts +25 -5
  3. package/agent-server.js +152 -7
  4. package/agent-store-backend.d.ts +6 -1
  5. package/agent-store-backend.js +861 -55
  6. package/application-api.d.ts +19 -1
  7. package/application-api.js +578 -21
  8. package/approvals.d.ts +2 -0
  9. package/approvals.js +12 -5
  10. package/assistant.d.ts +34 -8
  11. package/assistant.js +81 -26
  12. package/availability-store.d.ts +193 -0
  13. package/availability-store.js +558 -0
  14. package/booking-rules.d.ts +92 -5
  15. package/booking-rules.js +243 -15
  16. package/dashboard-assistant-ui.d.ts +1 -1
  17. package/dashboard-assistant-ui.js +109 -8
  18. package/dashboard.d.ts +55 -6
  19. package/dashboard.js +790 -105
  20. package/ddl.d.ts +5 -1
  21. package/ddl.js +760 -9
  22. package/event-feed.d.ts +1 -1
  23. package/event-feed.js +18 -0
  24. package/holiday-import.d.ts +38 -0
  25. package/holiday-import.js +57 -0
  26. package/holidays.d.ts +38 -0
  27. package/holidays.js +150 -0
  28. package/hours.d.ts +125 -0
  29. package/hours.js +224 -0
  30. package/index.d.ts +14 -3
  31. package/index.js +10 -2
  32. package/interop.d.ts +25 -7
  33. package/interop.js +145 -17
  34. package/lifecycle.d.ts +31 -0
  35. package/lifecycle.js +115 -0
  36. package/maintenance.d.ts +19 -0
  37. package/maintenance.js +55 -1
  38. package/metrics.js +3 -0
  39. package/openapi.json +16237 -7795
  40. package/package.json +1 -1
  41. package/ranges.d.ts +28 -0
  42. package/ranges.js +135 -0
  43. package/resource-search.d.ts +19 -0
  44. package/resource-search.js +153 -0
  45. package/rest-api.js +7 -0
  46. package/rules.d.ts +2 -0
  47. package/rules.js +2 -0
  48. package/self-host.d.ts +1 -1
  49. package/self-host.js +221 -21
  50. package/slotlock.manifest.json +1 -1
  51. package/store.d.ts +367 -24
  52. package/store.js +1567 -97
  53. package/types.d.ts +198 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,243 @@
3
3
  All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and semantic
4
4
  versioning.
5
5
 
6
+ ## 0.9.0 - 2026-11-11
7
+
8
+ ### Added
9
+
10
+ - Each hold's payment is kept apart from the hold (new table `hold_payments`, under RLS, erased with
11
+ the tenant), so `slotlock_mark_hold_paid` with the same payment answers `replayed: true` however
12
+ the hold ended: confirmed in place or booked as an event. Before, a confirmed hold answered
13
+ `404 hold_not_found`, which `@slotlock/payments-stripe` with `refundRefused` refunded: a customer
14
+ reloading the return page, or Stripe redelivering a webhook, got a booked time refunded (found by
15
+ the security audit). The adapter now refunds only `hold_expired`, `payment_conflict` and
16
+ `payment_amount_mismatch`.
17
+ - Three integration packages in `packages/` (RFC 0004 P3), each its own process with its own
18
+ schema and secrets, talking to Slotlock over its API:
19
+ - `@slotlock/sync` (C1, C4, T2): busy time from Google freeBusy, Microsoft getSchedule and
20
+ iCalendar URLs, recorded as each source's busy time; confirmed bookings written back to Google
21
+ and Microsoft calendars, times only. OAuth with PKCE, then a signed page where the tenant's person
22
+ chooses which calendar blocks which resource; tokens, URLs and keys sealed with AES-256-GCM. A
23
+ booking changed in the calendar is reported on the feed (`coverage.conflict`), never applied. A
24
+ provider's 429 or 503 is retried when it says, else with backoff; a feed cursor that expired
25
+ while the sync was down is recovered by reconciling each calendar with the bookings as they are.
26
+ - `@slotlock/notify` (C2): confirmations, moves, cancellations and reminders by Postmark,
27
+ Resend or SES, and Twilio SMS and WhatsApp, each send recorded under its key (change id,
28
+ channel, recipient) before it is made. Consent per channel; test tenants go to a capture inbox.
29
+ Recipients are kept only as digests, except an event's attendees, kept sealed until a day after
30
+ it ends so a cancellation reaches them.
31
+ - `@slotlock/payments-stripe` (C3, T2): a PaymentIntent read back from the tenant's own Stripe
32
+ account marks its hold paid, with the amount received; a payment for an expired hold is refused
33
+ and, if asked, refunded.
34
+ - Each runs as its own process (`slotlock-sync`, `slotlock-notify`, `slotlock-payments-stripe`,
35
+ each with `migrate` and `serve`), and `packages/integrations.Dockerfile` builds one image with
36
+ all three.
37
+ - What integrations build on (RFC 0004 P3, D1's recommendation: core keeps primitives, packages
38
+ outside it send and sync). Feed type `reminder.due`: maintenance puts each event reminder on the
39
+ feed once, when it comes due (within the last day, before the event starts), and again if the
40
+ occurrence moves; `reminders_due` in its answer. A change's `id` is the key a sender sends it
41
+ once by. Events take `notify` (`slotlock_create_event`'s `notify: false`): no reminders, and a
42
+ sender tells no one. Reservations take `reminder_minutes` (up to five, minutes before the start),
43
+ each said once as `reminder.due` for the contact the reservation names, and again if it moves.
44
+ - Contacts (P3, T2): new table `contacts`, the people a booking may name to be told about it, by
45
+ the tenant's own reference, with email, phone and WhatsApp numbers (E.164), locale and consent
46
+ per channel. `slotlock_put_contact`, `slotlock_get_contact`, `slotlock_delete_contact`
47
+ (`/v1/contacts/{contact_ref}`); deleting one also takes its reference off every booking. At most
48
+ 100,000 a tenant. `slotlock_list_contacts` (`GET /v1/contacts`) lists them a page at a time, and
49
+ each answers `consent_at`: when each channel's answer was given, kept while it stands. The
50
+ dashboard's Contacts page lists them and forgets one, for an erasure request. Reservations take
51
+ `contact_ref` and `notify`, both on the feed with each change; neither is ever the person.
52
+ - Holds that wait for payment (RFC 0004 C3, T2): `payment_required` on `slotlock_acquire_hold`
53
+ (time to live at most 30 minutes). Confirming one answers `409 payment_required` until
54
+ `slotlock_mark_hold_paid` (`POST /v1/holds/{hold_id}/paid`) records the payment's reference at
55
+ the integrator's processor: the same reference again answers the same hold, another
56
+ `payment_conflict`, an expired hold `hold_expired`. New feed type `hold.paid`. Slotlock records
57
+ payment state and never moves money. A hold may name what the payment must be (`payment`:
58
+ `amount_minor` and `currency`); `slotlock_mark_hold_paid` then takes what was `received`, and
59
+ less, or another currency, is `409 payment_amount_mismatch`. Holds answer `payment_amount` and
60
+ `paid_amount`. Agents' `slotlock_hold_slots` takes `payment_required` and `payment` too.
61
+ - Busy time from calendars that give no events (RFC 0004 C1, T2): `slotlock_put_source_busy`
62
+ (`PUT /v1/resources/{resource_id}/coverage/{source}/busy`) records a source's coverage of a
63
+ window and the busy time in it (at most 5,000 intervals, merged), replacing what it said of that
64
+ window. New table `source_busy`. Every read counts it (free/busy, a resource's busy time,
65
+ searches and waitlist offers), and every booking Slotlock makes is refused
66
+ over it (`overlap`). A confirmed booking the source newly reports busy time over is left alone
67
+ and reported once as `coverage.conflict` (named by `source_id`, never the source). Retiring the
68
+ source forgets its busy time. `slotlock_report_coverage_conflict`
69
+ (`POST /v1/resources/{resource_id}/coverage/{source}/conflicts`) lets a calendar sync report a
70
+ booking it wrote that was moved (`changed_in_calendar`) or removed (`removed_from_calendar`) in
71
+ the calendar, as `coverage.conflict` with the booking left as it is.
72
+ - Hours that inherit (RFC 0004 B2). The tenant and each location store weekly hours
73
+ (`setTenantAvailability`, the new `setLocationAvailability`; `PUT /v1/availability`,
74
+ `PUT /v1/locations/{location}/availability`, `GET /v1/availability`). A resource without its own
75
+ uses its location's (the nearest up the chain), then the tenant's, then the server's, and a
76
+ resource added later inherits too. Resources carry `inherited_availability`. The dashboard has
77
+ "Your hours" and an Hours page for each location; the CLI has `hours set --location`.
78
+ - Closures and special days (RFC 0004 B1, T2): new table `availability_exceptions`, for the tenant,
79
+ a location (and everything inside it) or one resource. Each replaces whole local days, read in
80
+ each resource's time zone: `closed` empties them, `open` gives them 1-4 windows of their own.
81
+ The nearest scope wins on a day. One evaluator (`openWindows`) serves search, pool availability,
82
+ approval policies' `outside_hours`, and writes under `enforceHours`. At most 5,000 a tenant
83
+ (`availability_exceptions_limit_reached`). API: list, create and delete under
84
+ `/v1/availability/exceptions`. Dashboard: a Closures page. CLI: `slotlock closures
85
+ list|add|delete|import`.
86
+ - Holiday calendars (B1): `slotlock_import_holidays` (`POST /v1/availability/holidays`) and
87
+ `closures import` close the days an iCalendar file's all-day events cover, recurring ones
88
+ expanded over a bounded span. `serve` fetches an `https` URL through the guarded fetch (no
89
+ private addresses, 1 MB, 10 seconds); the store-backed application backend fetches none unless
90
+ given `holidayFetch`. Each import is a calendar (new table `holiday_calendars`, under RLS, erased
91
+ with the tenant), kept by name where it applies: its `name`, else its own `X-WR-CALNAME`, else
92
+ the URL. Importing it again updates its closures and removes those it no longer names in the
93
+ span (`removed`), so a holiday a calendar dropped goes too; a closure imported before calendars
94
+ were kept becomes the calendar's that names it. With `subscribe`, the URL is kept and maintenance
95
+ imports it again every 30 days (`holidayFetch` on `maintainSlotlockTenant` and the scheduler;
96
+ `serve` and `slotlock maintain` pass the guarded fetch), a failed import keeping its closures,
97
+ noting why (`last_error`) and trying again a day later. `slotlock_list_holiday_calendars`
98
+ (`GET /v1/availability/holiday-calendars`) and `slotlock_delete_holiday_calendar`
99
+ (`DELETE …/{calendar_id}`, its closures with it); closures answer `calendar_id`; at most 50
100
+ calendars a tenant. The dashboard's Closures page takes a file upload (the form was limited to
101
+ 8 KB, too small for most calendars, and now takes 1 MB), a paste or, where the server fetches
102
+ them (`serve`, Cloud), an address with "Import it again every 30 days", and lists the calendars
103
+ with their next import, a failed one's error, and Delete. `closures import` names the calendar
104
+ with `--label`.
105
+ - Durations and hires (RFC 0004 B3). Booking rules gain `minDurationMinutes`, `maxDurationMinutes`,
106
+ `allowedDurations` and `defaultDurationMinutes`, and for a booking from one local day to another
107
+ `pickupTimes` and `returnTimes` (it starts and ends in them, and under `enforceHours` needs only
108
+ its start and end inside the hours). `turnaroundMinutes` goes up to a week. Refusals:
109
+ `booking_rule_min_duration`, `booking_rule_max_duration`, `booking_rule_duration_not_allowed`,
110
+ `booking_rule_pickup_time`, `booking_rule_return_time`. Searches offer only allowed lengths, and
111
+ take `defaultDurationMinutes` when they name none (`400 duration_required` otherwise).
112
+ `hoursApply` (`hours_apply`) says how the hours hold a hire: `whole` (all of it) or `ends` (its
113
+ start and end only), by default `ends` with pick-up or return times, else `whole`. Writes follow
114
+ it under `enforceHours`; `slotlock_find_available_slots` and `slotlock_find_next_available`
115
+ follow it always, so with `ends` they offer slots from one day to another, judged as the write
116
+ would be, and `slotlock_find_available_ranges` judges ranges as the write would where hours are
117
+ enforced. A pick-up or return refusal comes with alternatives, as other refusals of a time do.
118
+ - Zone rule changes reach everything (RFC 0004 B9): `followZoneRules` also re-checks a recurring
119
+ series' occurrences to come (each kept with its wall time, reported once as
120
+ `event.timezone_shifted` with its `recurrence_id`; with `move`, the whole series moves at its
121
+ next revision in one transaction, each occurrence and its booking keeping its `recurrence_id`,
122
+ but only where the server's zone rules agree with the database's, else each is reported with
123
+ `reason: "rules_disagree"`; something in the way keeps its times, said once as
124
+ `event.timezone_conflict`), and reservations written with the new
125
+ optional `local_start` (`localStart`), reported as `reservation.timezone_shifted` or, with
126
+ `move`, moved at the next revision (`reservation.timezone_conflict` when something is in the
127
+ way). A `local_start` that does not name the reservation's start is `400 invalid_local_start`.
128
+ - Holds across resources (RFC 0004 B7, T2): `slotlock_hold_together` (`POST /v1/slot-holds/together`),
129
+ the 19th agent tool, holds one time on 2-20 resources all or nothing, as one group whose holds
130
+ expire together. `slotlock_book_together` takes its `hold_group_id` and books the set from those
131
+ holds in one transaction. The application API gains `slotlock_tenant_book_together`
132
+ (`POST /v1/tenant/bundles`) and `slotlock_tenant_book_pool` (`POST /v1/tenant/pool-bookings`), the
133
+ agents' bundle and pool bookings for the tenant's own systems. Their events are the tenant's
134
+ (owner `internal`), not the key's: no agent's event quota counts them, and revoking the key
135
+ leaves them. Approval policies hold them only when they say `include_application: true` (new,
136
+ false by default), and a grant books them as the tenant's again; a key that `requires_approval`
137
+ still waits for a person whatever it books.
138
+ - Waitlists that match (RFC 0004 B6): an entry may wait for any slot of a length within its
139
+ window (`duration_minutes`, `durationMs`), on a resource or a pool, and is offered the earliest
140
+ free one. Offers end `accepted`, `declined` (new `slotlock_decline_waitlist_offer`,
141
+ `POST /v1/waitlist/{entry_id}/decline`) or `lapsed`, decided by what becomes of their hold, by a
142
+ database trigger whatever path takes it. New feed types `waitlist.joined`, `.accepted`,
143
+ `.declined`, `.lapsed` and `.left`. Entries answer `duration_minutes` and `decided_at`.
144
+ - A written booking lifecycle (RFC 0004 B4, T2): `src/lifecycle.ts` names each status, the move
145
+ each action makes and the refusal every other gets, and a test holds the store to it. A
146
+ tombstone keeps why the booking ended (`end_reason`: `cancelled` or `no_show`). New feed types:
147
+ `reservation.rescheduled` (a move, as well as `reservation.updated`) and `reservation.completed`,
148
+ which maintenance writes once for each booking whose end passed in the last week
149
+ (`bookings_completed` in its answer).
150
+ - Cancellation and reschedule policies (RFC 0004 B5, T2): rules `cancelCutoffMinutes` and
151
+ `rescheduleCutoffMinutes` (`booking_rule_cancel_cutoff`, `booking_rule_reschedule_cutoff`), which
152
+ an admin may go past with `override_reason`, kept on the tombstone and the feed; and
153
+ `cancellationFee`, recorded on a late cancellation for the integrator to charge.
154
+ - `slotlock_find_available_ranges` (`GET /v1/available-ranges`), the 18th agent tool: hires across
155
+ days for named resources or a pool's members, the earliest range on each from a pick-up window,
156
+ lasting N days or ending in a return window.
157
+ - VAVAILABILITY carries RDATE and EXDATE (B1): the export adds the next year's closures as EXDATEs
158
+ and special hours as AVAILABLEs with RDATEs. The import reads them, and single AVAILABLEs, as
159
+ the resource's own exceptions, replacing those an earlier import made.
160
+
161
+ - **Chats in the dashboard assistant.** Each conversation is a chat a member keeps:
162
+ - it is titled by its first message, at its own address (`<root>/assistant?c=<id>`);
163
+ - chats are listed beside the one shown, most recent first, grouped by day (Today, Yesterday,
164
+ Previous 7 days, Previous 30 days, Older), with a search box, and **Rename** and **Delete**
165
+ behind each one's menu;
166
+ - **New chat** starts another, and on a narrow screen the list is a page of its own
167
+ (`<root>/assistant/chats`);
168
+ - the page opens on the chat used in the past two hours, else on a new one;
169
+ - a streamed reply tells the page which chat it went to, so a new one gets its address and its
170
+ place in the list without a reload.
171
+
172
+ Chats are kept until the member deletes one, at most 100 per member (`SLOTLOCK_ASSISTANT_MAX_CHATS`;
173
+ past it, the least recently used goes, even with many started at once). A chat deleted while its
174
+ reply is being written stays deleted. They live in the new table `slotlock.assistant_chats`,
175
+ under forced RLS, erased with the tenant. `slotlock migrate` makes each member's 0.8 conversation
176
+ their first chat, once. Run `slotlock migrate`.
177
+ - **Finding a resource by name.** `slotlock_list_resources` takes `query` and ranks the resources
178
+ that match what a person calls one, best first:
179
+ - each word is looked for in the name, reference, labels, kind, location and description;
180
+ - case and accents are ignored, and a typo, a word cut short or words run together are forgiven
181
+ (`mercedez`, `merc 2025` and `mercedesbenz` all find "Mercedes-Benz 2025");
182
+ - numbers match exactly;
183
+ - a search does bounded work, whatever the resources hold: past a budget of typo checks, it
184
+ matches exactly, by prefix and by words run together only.
185
+
186
+ `resource_not_found` for a name or reference given where an id belongs names the likeliest
187
+ resources in `details.candidate_1` to `candidate_3`.
188
+
189
+ ### Changed
190
+
191
+ - The tenant's hours are stored, not copied onto every resource (RFC 0004 B2), and resources
192
+ without their own inherit them, those added later too. `storeTenantAvailability` stores them and
193
+ answers `{ inheriting, ownHours }`. `setTenantAvailability` keeps its 0.8.0 contract, deprecated:
194
+ every resource uses the hours afterwards, and it answers how many. `PUT /v1/availability` keeps
195
+ the hours resources have of their own by default; its `override_resources` is `none`, `all`
196
+ (clear them) or `matching` (clear those the same as the tenant's were or as the new ones: the
197
+ copies earlier versions made, while hours set apart on purpose stay), as are
198
+ `hours set --all --override` and `--matching`. It answers `resources_updated` (resources the
199
+ hours now apply to) and `resources_with_own_hours`. The dashboard's "Your hours" names the
200
+ resources that keep their own, with the same three choices, and `/resources?hours=own` lists
201
+ them.
202
+ - `slotlock_run_maintenance` answers `bookings_completed`; the five clients' checks of its keys
203
+ include it. `slotlock_cancel_reservation` and `slotlock_cancel_tenant_event` answer
204
+ `cancellation_fee` (null when none).
205
+ - `parseVAvailability` answers `days` as well as `rules`, and no longer refuses RDATE, EXDATE or an
206
+ AVAILABLE without RRULE.
207
+
208
+ - **The dashboard assistant's store** (`SlotlockAssistantStore`) keeps chats. `listChats`,
209
+ `getChat`, `putChat`, `renameChat` and `deleteChat` replace `getConversation`,
210
+ `putConversation` and `deleteConversation`, and `settleAction` takes the chat's id. The tenant
211
+ store has `listAssistantChats`, `getAssistantChat`, `putAssistantChat`, `renameAssistantChat`
212
+ and `deleteAssistantChat`, and `settleAssistantAction` takes `chatId`.
213
+ - **Conversation lifetime.** `conversationTtlMs` no longer defaults to 2 hours: without it, chats
214
+ are kept until deleted. Given, it may be up to 3,650 days.
215
+ - **New conversation is New chat.** It starts another chat and deletes nothing. A form of 0.8 that
216
+ posts `<root>/assistant/reset` does the same.
217
+ - **The assistant's system prompt** lists the workspace's resources (up to 50, with the ids tools
218
+ take) and says how to find others. The tool descriptions and the MCP instructions say that
219
+ `resource_id` takes an id and how to find one, and that a filter leaves out resources that state
220
+ nothing.
221
+
222
+ ### Fixed
223
+
224
+ - Enforced hours (`enforceHours`) judged a write inside `withTenant` against no hours when its
225
+ resource used the server's default (`defaultAvailabilityRules`, `SLOTLOCK_AVAILABILITY` under
226
+ `serve`): every such booking was refused `booking_rule_outside_hours`. The tenant's store now
227
+ carries the server's hours.
228
+
229
+ - **The assistant's chips stream.** "What's on today" and "Find free time" were forms that waited
230
+ for the whole turn and then reloaded the page; they now send through the box, so their reply
231
+ streams.
232
+
233
+ - The dashboard assistant's Stop stops the turn on any runtime. Cancelling the streamed reply now
234
+ aborts the turn itself, which keeps what it had. Before, only `request.signal` stopped it, and a
235
+ runtime that does not abort it when the client goes kept the turn running. On Workers, the
236
+ request's database connection now closes only once the stopped turn is kept.
237
+ - The Overview never shows a change by its type. A change it has no words for, from a newer server,
238
+ reads "Something changed", on its resource when it names one. A type such as `constructor` no
239
+ longer finds an inherited entry.
240
+ - The assistant page's script keeps live steps in a map with no prototype, so a provider's tool-call
241
+ id of `__proto__` names nothing.
242
+
6
243
  ## 0.8.0 - 2026-11-10
7
244
 
8
245
  ### Added
package/agent-server.d.ts CHANGED
@@ -39,6 +39,8 @@ export declare const SLOTLOCK_A2A_TASK_PAGE_SIZE: Readonly<{
39
39
  }>;
40
40
  /** The A2A 1.0 `TaskState` names, as a `ListTasks` status filter spells them in JSON. */
41
41
  export declare const SLOTLOCK_A2A_TASK_STATES: readonly ["TASK_STATE_UNSPECIFIED", "TASK_STATE_SUBMITTED", "TASK_STATE_WORKING", "TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED", "TASK_STATE_INPUT_REQUIRED", "TASK_STATE_REJECTED", "TASK_STATE_AUTH_REQUIRED"];
42
+ /** The longest resource search (resource-search.ts SLOTLOCK_RESOURCE_SEARCH_MAX; a test holds them equal). */
43
+ export declare const SLOTLOCK_AGENT_RESOURCE_SEARCH_MAX = 200;
42
44
  /** The tools' copy of the profile's kinds and bounds, for the test that holds it to resource-profile.ts. */
43
45
  export declare const SLOTLOCK_AGENT_RESOURCE_PROFILE: Readonly<{
44
46
  kinds: ("desk" | "equipment" | "machine" | "other" | "person" | "room" | "service" | "space" | "vehicle")[];
@@ -62,7 +64,7 @@ export declare const SLOTLOCK_MAX_ALTERNATIVES = 3;
62
64
  * Operation (MCP tool / A2A skill) names. They use only `[a-z_]`, so they are valid tool names for the
63
65
  * Claude Messages API (`^[a-zA-Z0-9_-]{1,128}$`), OpenAI function calling and every MCP host.
64
66
  */
65
- export type SlotlockAgentOperation = 'slotlock_list_resources' | 'slotlock_get_free_busy' | 'slotlock_find_next_available' | 'slotlock_find_available_slots' | 'slotlock_hold_slots' | 'slotlock_release_slots' | 'slotlock_confirm_slot' | 'slotlock_reschedule' | 'slotlock_cancel' | 'slotlock_book_together' | 'slotlock_book_pool' | 'slotlock_get_pool_availability' | 'slotlock_create_event' | 'slotlock_get_event' | 'slotlock_list_events' | 'slotlock_update_event' | 'slotlock_delete_event';
67
+ export type SlotlockAgentOperation = 'slotlock_list_resources' | 'slotlock_get_free_busy' | 'slotlock_find_next_available' | 'slotlock_find_available_slots' | 'slotlock_find_available_ranges' | 'slotlock_hold_slots' | 'slotlock_hold_together' | 'slotlock_release_slots' | 'slotlock_confirm_slot' | 'slotlock_reschedule' | 'slotlock_cancel' | 'slotlock_book_together' | 'slotlock_book_pool' | 'slotlock_get_pool_availability' | 'slotlock_create_event' | 'slotlock_get_event' | 'slotlock_list_events' | 'slotlock_update_event' | 'slotlock_delete_event';
66
68
  /**
67
69
  * Names earlier builds published, which every entry point still accepts and dispatches as the same
68
70
  * operation; nothing advertises them. The dotted names came first, before tool names had to be
@@ -189,6 +191,12 @@ export interface SlotlockAgentCalendarBackend {
189
191
  * (`slotlock_hold_slots`); a tool without it answers `holds_unavailable`.
190
192
  */
191
193
  holdSlots?(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
194
+ /**
195
+ * Optional. Hold one time on 2-20 resources all or nothing, as one group with one time to live
196
+ * (`slotlock_hold_together`, RFC 0004 B7), else `holds_unavailable`. `slotlock_book_together`
197
+ * with its `hold_group_id` books the set.
198
+ */
199
+ holdTogether?(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
192
200
  /** Optional. Book any one free member of a pool (`slotlock_book_pool`), else `pools_unavailable`. */
193
201
  bookPool?(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
194
202
  /**
@@ -228,6 +236,18 @@ export interface SlotlockAgentCalendarBackend {
228
236
  }[];
229
237
  certain: boolean;
230
238
  }>;
239
+ /**
240
+ * Optional. Hires across days (`slotlock_find_available_ranges`, RFC 0004 B3): each resource's, or
241
+ * each pool member's, earliest range from a pick-up window to a return window, else
242
+ * `ranges_unavailable`.
243
+ */
244
+ findAvailableRanges?(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
245
+ /**
246
+ * Optional. The length a search for these resources offers when it is asked for none: their
247
+ * booking rules' `defaultDurationMinutes` when they all give the same one, else null. Without it,
248
+ * a search names its length.
249
+ */
250
+ defaultDuration?(context: SlotlockAgentInvocationContext, resourceIds: readonly string[]): Promise<number | null>;
231
251
  }
232
252
  /**
233
253
  * OAuth 2.0 discovery for the MCP endpoint (MCP 2025-11-25 authorization, RFC 9728). Slotlock only
@@ -593,11 +613,11 @@ export declare function slotlockOpenApi(options?: {
593
613
  application?: boolean;
594
614
  }): Record<string, unknown>;
595
615
  export declare function slotlockAgentTools(): {
596
- name: "slotlock_book_pool" | "slotlock_book_together" | "slotlock_cancel" | "slotlock_confirm_slot" | "slotlock_create_event" | "slotlock_delete_event" | "slotlock_find_available_slots" | "slotlock_find_next_available" | "slotlock_get_event" | "slotlock_get_free_busy" | "slotlock_get_pool_availability" | "slotlock_hold_slots" | "slotlock_list_events" | "slotlock_list_resources" | "slotlock_release_slots" | "slotlock_reschedule" | "slotlock_update_event";
597
- title: "Book from a pool" | "Book resources together" | "Cancel an event" | "Confirm a held slot" | "Create resource event" | "Delete resource event" | "Find available slots" | "Find next available interval" | "Get free-busy" | "Get pool availability" | "Get resource event" | "Hold candidate slots" | "List calendar resources" | "List resource events" | "Release held slots" | "Reschedule an event" | "Update resource event";
598
- description: "Book a resource for a time, as an event of up to 3,660 days (a booking, lease or long rental). The database refuses a time already taken: 409 overlap, with alternatives (the earliest free times of the same length) and, when the booking in the way is yours, its id. A booking rule that refuses the time answers booking_rule_… with alternatives too. To book a time you hold, pass its hold_id, or use slotlock_confirm_slot. A retry with the same idempotency_key answers the same event." | "Book a time you hold (a hold_id from slotlock_hold_slots): the held resource and time exactly, with the title and details you give, and the rest of its group released. 409 hold_expired when the hold lapsed first; 404 hold_not_found when it was released, booked or is not yours. A retry answers the same event (replayed)." | "Book any one member of a pool (a vehicle class, a room type) for a time: members are tried in the pool's order (by id, fewest bookings in the last 30 days first, or in turn, as the tenant sets it) and the first that is free and allows the booking is booked, in one transaction, so a class sells out exactly at its number of members. The event names the member booked. 409 pool_full when none is free; 404 pool_not_found when the pool has no member you may book. A retry with the same idempotency_key answers the same event." | "Book the same time on 2-20 resources at once, all or none: a van with its driver, a room with its projector. If any of them is taken, nothing is booked, and the refusal names that resource_id. Find such a time with slotlock_find_available_slots and require_free. A retry with the same idempotency_key answers the same events." | "Cancel one of your events at the revision you last read (expected_revision); a stale one is 409 revision_conflict. It cannot come back at that revision, so an old copy synced later does not restore it. slotlock_cancel does the same with fewer fields." | "Cancel one of your events at the revision you last read (expected_revision); a stale one is 409 revision_conflict. Its time is free at once. A retry answers the same cancellation (replayed)." | "Change one of your events at the revision you last read (expected_revision): its time, resource or details, all or nothing. A time already taken is 409 overlap with alternatives; a stale revision is 409 revision_conflict (read the event again). With expected_resource_id, the resource you read it on, an event since moved elsewhere is not found. To change only the time, slotlock_reschedule is simpler." | "Find the earliest certain interval that satisfies the requested duration (up to the 367-day search window, so a multi-month rental slot can be found). Name the resources with resource_ids, or select them as slotlock_get_free_busy does: at most 100." | "Hold up to five candidate slots for a short time (30-900 seconds) while a person chooses, as alternatives in one group: each free one is held, the others are listed with why not. Book one by passing its hold_id to slotlock_create_event, which releases the rest; unbooked holds expire on their own. A hold is yours alone, and you may have 20 live at once. A candidate refused for its time comes with alternatives (the earliest free times of its length). slotlock_confirm_slot books a held time." | "How many members of a pool (a vehicle class, a room type) are free across a window, each only where its booking rules let a booking start: the window split where that number changes, each part with how many are free, and coverage certainty. Book one with slotlock_book_pool. 404 pool_not_found when the pool has no member you may book." | "List resources visible to the authenticated tenant principal, with what each says about itself: its name (show it rather than the reference), description, kind, capacity and labels, where it is (location), the groups it is in (a team, a department), and the pool it belongs to (a vehicle class, a room type). With kind, only resources of that kind; with labels, only those with every label given; with location, only those there or at a location in it; with group, only that group's. With pool, only that pool's members, in the order slotlock_book_pool tries them." | "List up to `limit` certain slots of the requested duration, earliest first across the resources, each one free of busy time and inside the resource's hours, to offer a person a choice. With require_free, only times when at least that many of the resources are free together, each slot naming them in resource_ids: every resource for a booking that needs them all (book it with slotlock_book_together), or any n of them. No slot is listed unless every resource's coverage is certain. Name the resources with resource_ids, or select them as slotlock_get_free_busy does: at most 20." | "List your events that meet a time window, earliest first." | "Move one of your events to a new time, and to another resource if you give one, at the revision you last read (expected_revision). A time already taken is 409 overlap with alternatives; a stale revision 409 revision_conflict. A retry answers the same move (replayed)." | "Read one of your events by its id, with its revision (pass it as expected_revision to change or cancel it)." | "Release one of your holds, and the rest of its group, at once instead of letting them expire. `released` is false when it was already gone." | "When each resource is busy in a window, as busy intervals only (never whose or what), and whether that is certain (coverage: \"uncertain\" where a calendar feeding it has not been read lately). Name the resources with resource_ids, or select them instead by location (and every location in it), group, labels (every one), kind or pool: at most 100 (more answers selection_too_large, none selection_empty), named in selected_resource_ids.";
616
+ name: "slotlock_book_pool" | "slotlock_book_together" | "slotlock_cancel" | "slotlock_confirm_slot" | "slotlock_create_event" | "slotlock_delete_event" | "slotlock_find_available_ranges" | "slotlock_find_available_slots" | "slotlock_find_next_available" | "slotlock_get_event" | "slotlock_get_free_busy" | "slotlock_get_pool_availability" | "slotlock_hold_slots" | "slotlock_hold_together" | "slotlock_list_events" | "slotlock_list_resources" | "slotlock_release_slots" | "slotlock_reschedule" | "slotlock_update_event";
617
+ title: "Book from a pool" | "Book resources together" | "Cancel an event" | "Confirm a held slot" | "Create resource event" | "Delete resource event" | "Find available ranges" | "Find available slots" | "Find next available interval" | "Get free-busy" | "Get pool availability" | "Get resource event" | "Hold candidate slots" | "Hold resources together" | "List calendar resources" | "List resource events" | "Release held slots" | "Reschedule an event" | "Update resource event";
618
+ description: "Book a resource for a time, as an event of up to 3,660 days (a booking, lease or long rental). The database refuses a time already taken: 409 overlap, with alternatives (the earliest free times of the same length) and, when the booking in the way is yours, its id. A booking rule that refuses the time answers booking_rule_… with alternatives too. To book a time you hold, pass its hold_id, or use slotlock_confirm_slot. A retry with the same idempotency_key answers the same event." | "Book a time you hold (a hold_id from slotlock_hold_slots): the held resource and time exactly, with the title and details you give, and the rest of its group released. 409 hold_expired when the hold lapsed first; 404 hold_not_found when it was released, booked or is not yours. A retry answers the same event (replayed)." | "Book any one member of a pool (a vehicle class, a room type) for a time: members are tried in the pool's order (by id, fewest bookings in the last 30 days first, or in turn, as the tenant sets it) and the first that is free and allows the booking is booked, in one transaction, so a class sells out exactly at its number of members. The event names the member booked. 409 pool_full when none is free; 404 pool_not_found when the pool has no member you may book. A retry with the same idempotency_key answers the same event." | "Book the same time on 2-20 resources at once, all or none: a van with its driver, a room with its projector. If any of them is taken, nothing is booked, and the refusal names that resource_id. Find such a time with slotlock_find_available_slots and require_free. With hold_group_id, it books the set slotlock_hold_together held, from those holds. A retry with the same idempotency_key answers the same events." | "Cancel one of your events at the revision you last read (expected_revision); a stale one is 409 revision_conflict. It cannot come back at that revision, so an old copy synced later does not restore it. slotlock_cancel does the same with fewer fields." | "Cancel one of your events at the revision you last read (expected_revision); a stale one is 409 revision_conflict. Its time is free at once. A retry answers the same cancellation (replayed)." | "Change one of your events at the revision you last read (expected_revision): its time, resource or details, all or nothing. A time already taken is 409 overlap with alternatives; a stale revision is 409 revision_conflict (read the event again). With expected_resource_id, the resource you read it on, an event since moved elsewhere is not found. To change only the time, slotlock_reschedule is simpler." | "Find hires across days, such as a car from Friday to Monday: for each resource named, or each member of a pool, the earliest range that starts between pickup_start and pickup_end and either lasts `days` days (ending at the same local time) or ends between return_start and return_end. Each range is free throughout with its buffers, allowed by the resource's booking rules (pick-up and return times, lengths, notice, step) and starts and ends inside its bookable hours. Up to `limit` ranges, earliest first, one per resource; none unless every resource's coverage is certain. Book one with slotlock_create_event, or a pool's with slotlock_book_pool, at its start and end." | "Find the earliest certain interval that satisfies the requested duration (up to the 367-day search window, so a multi-month rental slot can be found). Name the resources with resource_ids, or select them as slotlock_get_free_busy does: at most 100." | "Hold one time on 2-20 resources at once, all or none, such as a car with its driver while a person decides: if any is taken, nothing is held and the refusal names that resource_id. The holds are one group with one time to live (ttl_seconds, the earliest of them). Book the set with slotlock_book_together and its hold_group_id; release them with slotlock_release_slots and any hold_id. A retry with the same idempotency_key answers the same holds." | "Hold up to five candidate slots for a short time (30-900 seconds) while a person chooses, as alternatives in one group: each free one is held, the others are listed with why not. Book one by passing its hold_id to slotlock_create_event, which releases the rest; unbooked holds expire on their own. With payment_required, a hold is booked only once the system you book for records its payment (the person pays there): booking it before then answers payment_required, so send the person to pay and try again. A hold is yours alone, and you may have 20 live at once. A candidate refused for its time comes with alternatives (the earliest free times of its length). slotlock_confirm_slot books a held time." | "How many members of a pool (a vehicle class, a room type) are free across a window, each only where its booking rules let a booking start: the window split where that number changes, each part with how many are free, and coverage certainty. Book one with slotlock_book_pool. 404 pool_not_found when the pool has no member you may book." | "List resources visible to the authenticated tenant principal, with what each says about itself: its name (show it rather than the reference), description, kind, capacity and labels, where it is (location), the groups it is in (a team, a department), and the pool it belongs to (a vehicle class, a room type). With kind, only resources of that kind; with labels, only those with every label given; with location, only those there or at a location in it; with group, only that group's. With pool, only that pool's members, in the order slotlock_book_pool tries them. With query, the resources that best match what a person calls one, best first, at most limit and with no next_cursor: each word is looked for in the name, reference, labels, kind, location and description, ignoring case and accents, and forgiving a typo, a word cut short or words run together (\"mercedez\", \"merc 2025\" and \"mercedesbenz\" find \"Mercedes-Benz 2025\"); numbers match exactly. It is the way to find the id of a resource a person names, since other tools take ids only. When several match about equally, ask which one is meant. A resource that states no kind, labels or location is left out by those filters, so search without them before saying a resource does not exist." | "List up to `limit` certain slots of the requested duration, earliest first across the resources, each one free of busy time and inside the resource's hours, to offer a person a choice. With require_free, only times when at least that many of the resources are free together, each slot naming them in resource_ids: every resource for a booking that needs them all (book it with slotlock_book_together), or any n of them. No slot is listed unless every resource's coverage is certain. Name the resources with resource_ids, or select them as slotlock_get_free_busy does: at most 20." | "List your events that meet a time window, earliest first." | "Move one of your events to a new time, and to another resource if you give one, at the revision you last read (expected_revision). A time already taken is 409 overlap with alternatives; a stale revision 409 revision_conflict. A retry answers the same move (replayed)." | "Read one of your events by its id, with its revision (pass it as expected_revision to change or cancel it)." | "Release one of your holds, and the rest of its group, at once instead of letting them expire. `released` is false when it was already gone." | "When each resource is busy in a window, as busy intervals only (never whose or what), and whether that is certain (coverage: \"uncertain\" where a calendar feeding it has not been read lately). Name the resources with resource_ids, or select them instead by location (and every location in it), group, labels (every one), kind or pool: at most 100 (more answers selection_too_large, none selection_empty), named in selected_resource_ids.";
599
619
  annotations: {
600
- title: "Book from a pool" | "Book resources together" | "Cancel an event" | "Confirm a held slot" | "Create resource event" | "Delete resource event" | "Find available slots" | "Find next available interval" | "Get free-busy" | "Get pool availability" | "Get resource event" | "Hold candidate slots" | "List calendar resources" | "List resource events" | "Release held slots" | "Reschedule an event" | "Update resource event";
620
+ title: "Book from a pool" | "Book resources together" | "Cancel an event" | "Confirm a held slot" | "Create resource event" | "Delete resource event" | "Find available ranges" | "Find available slots" | "Find next available interval" | "Get free-busy" | "Get pool availability" | "Get resource event" | "Hold candidate slots" | "Hold resources together" | "List calendar resources" | "List resource events" | "Release held slots" | "Reschedule an event" | "Update resource event";
601
621
  readOnlyHint: boolean;
602
622
  destructiveHint: boolean;
603
623
  idempotentHint: true;