slotlock 0.8.0 → 0.10.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 (86) hide show
  1. package/CHANGELOG.md +542 -0
  2. package/NOTICE +2 -0
  3. package/README.md +4 -2
  4. package/agent-server.d.ts +25 -5
  5. package/agent-server.js +152 -7
  6. package/agent-store-backend.d.ts +6 -1
  7. package/agent-store-backend.js +1068 -68
  8. package/application-api.d.ts +28 -1
  9. package/application-api.js +789 -21
  10. package/approvals.d.ts +2 -0
  11. package/approvals.js +12 -5
  12. package/assistant.d.ts +34 -8
  13. package/assistant.js +81 -26
  14. package/availability-store.d.ts +193 -0
  15. package/availability-store.js +566 -0
  16. package/booking-rules.d.ts +103 -5
  17. package/booking-rules.js +263 -15
  18. package/config.d.ts +50 -0
  19. package/config.js +107 -0
  20. package/dashboard-assistant-ui.d.ts +1 -1
  21. package/dashboard-assistant-ui.js +109 -8
  22. package/dashboard-scope.d.ts +7 -0
  23. package/dashboard-scope.js +21 -0
  24. package/dashboard.d.ts +152 -8
  25. package/dashboard.js +3868 -399
  26. package/ddl-identity.d.ts +25 -0
  27. package/ddl-identity.js +881 -0
  28. package/ddl-scim.d.ts +5 -0
  29. package/ddl-scim.js +740 -0
  30. package/ddl-sso.d.ts +15 -0
  31. package/ddl-sso.js +1033 -0
  32. package/ddl.d.ts +44 -9
  33. package/ddl.js +2872 -47
  34. package/domains.d.ts +117 -0
  35. package/domains.js +340 -0
  36. package/event-feed.d.ts +2 -2
  37. package/event-feed.js +24 -34
  38. package/holiday-import.d.ts +38 -0
  39. package/holiday-import.js +56 -0
  40. package/holidays.d.ts +38 -0
  41. package/holidays.js +209 -0
  42. package/hours.d.ts +125 -0
  43. package/hours.js +224 -0
  44. package/index.d.ts +24 -5
  45. package/index.js +17 -3
  46. package/interop.d.ts +25 -7
  47. package/interop.js +145 -17
  48. package/invitation-email.d.ts +26 -0
  49. package/invitation-email.js +73 -0
  50. package/lifecycle.d.ts +31 -0
  51. package/lifecycle.js +115 -0
  52. package/maintenance.d.ts +26 -2
  53. package/maintenance.js +70 -2
  54. package/metrics.js +3 -0
  55. package/oidc.d.ts +135 -0
  56. package/oidc.js +452 -0
  57. package/openapi.json +20172 -9850
  58. package/package.json +2 -1
  59. package/ranges.d.ts +28 -0
  60. package/ranges.js +135 -0
  61. package/resource-search.d.ts +19 -0
  62. package/resource-search.js +153 -0
  63. package/rest-api.js +7 -0
  64. package/rules.d.ts +2 -0
  65. package/rules.js +2 -0
  66. package/scim-server.d.ts +19 -0
  67. package/scim-server.js +243 -0
  68. package/scim.d.ts +180 -0
  69. package/scim.js +416 -0
  70. package/self-host.d.ts +19 -6
  71. package/self-host.js +483 -28
  72. package/slotlock.manifest.json +1 -1
  73. package/store.d.ts +637 -23
  74. package/store.js +2529 -147
  75. package/types.d.ts +230 -0
  76. package/types.js +68 -1
  77. package/workspace-domains.d.ts +146 -0
  78. package/workspace-domains.js +249 -0
  79. package/workspace-roles.d.ts +47 -0
  80. package/workspace-roles.js +304 -0
  81. package/workspace-scim.d.ts +89 -0
  82. package/workspace-scim.js +157 -0
  83. package/workspace-sso.d.ts +131 -0
  84. package/workspace-sso.js +180 -0
  85. package/workspaces.d.ts +240 -0
  86. package/workspaces.js +433 -0
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;
package/agent-server.js CHANGED
@@ -61,6 +61,8 @@ export const SLOTLOCK_A2A_TASK_STATES = Object.freeze([
61
61
  ]);
62
62
  const DATE_TIME = z.string().datetime({ offset: true });
63
63
  const RESOURCE_ID = z.string().trim().min(1).max(200);
64
+ /** The longest resource search (resource-search.ts SLOTLOCK_RESOURCE_SEARCH_MAX; a test holds them equal). */
65
+ export const SLOTLOCK_AGENT_RESOURCE_SEARCH_MAX = 200;
64
66
  const EVENT_ID = z.string().trim().min(1).max(500);
65
67
  const HOLD_ID = z.string().uuid();
66
68
  const POOL = z.string().regex(/^[a-z0-9][a-z0-9._-]{0,99}$/);
@@ -112,6 +114,18 @@ function bookPool(backend, context, input) {
112
114
  throw new SlotlockAgentOperationError('pools_unavailable', 409);
113
115
  return backend.bookPool(context, input);
114
116
  }
117
+ /** `slotlock_hold_together`, from a backend that holds sets (RFC 0004 B7). */
118
+ function holdTogether(backend, context, input) {
119
+ if (!backend.holdTogether)
120
+ throw new SlotlockAgentOperationError('holds_unavailable', 409);
121
+ return backend.holdTogether(context, input);
122
+ }
123
+ /** `slotlock_find_available_ranges`, from a backend that searches hires (RFC 0004 B3). */
124
+ function findAvailableRanges(backend, context, input) {
125
+ if (!backend.findAvailableRanges)
126
+ throw new SlotlockAgentOperationError('ranges_unavailable', 409);
127
+ return backend.findAvailableRanges(context, input);
128
+ }
115
129
  /** `slotlock_get_pool_availability`, from a backend that knows pools, as `holdSlots`. */
116
130
  function getPoolAvailability(backend, context, input) {
117
131
  if (!backend.getPoolAvailability)
@@ -226,6 +240,8 @@ const ALTERNATIVE_REFUSALS = new Set([
226
240
  'booking_rule_max_advance',
227
241
  'booking_rule_slot_step',
228
242
  'booking_rule_outside_hours',
243
+ 'booking_rule_pickup_time',
244
+ 'booking_rule_return_time',
229
245
  ]);
230
246
  /** At most this many alternatives, the earliest first. */
231
247
  export const SLOTLOCK_MAX_ALTERNATIVES = 3;
@@ -263,7 +279,8 @@ async function alternativesFor(backend, context, want) {
263
279
  const found = await findAvailableSlots(backend, { ...context, operation: 'slotlock_find_available_slots' }, {
264
280
  resource_ids: resourceIds,
265
281
  start: new Date(from).toISOString(),
266
- end: new Date(from + ALTERNATIVES_WINDOW_MS).toISOString(),
282
+ // Starts over the next seven days, however long the booking runs.
283
+ end: new Date(from + ALTERNATIVES_WINDOW_MS + durationMs).toISOString(),
267
284
  duration_minutes: durationMs / 60_000,
268
285
  limit: SLOTLOCK_MAX_ALTERNATIVES,
269
286
  ...(want.needed !== undefined ? { require_free: Math.min(want.needed, resourceIds.length) } : {}),
@@ -542,7 +559,9 @@ const OPERATION_SCOPES = Object.freeze({
542
559
  slotlock_get_free_busy: 'read',
543
560
  slotlock_find_next_available: 'read',
544
561
  slotlock_find_available_slots: 'read',
562
+ slotlock_find_available_ranges: 'read',
545
563
  slotlock_hold_slots: 'write',
564
+ slotlock_hold_together: 'write',
546
565
  slotlock_book_together: 'write',
547
566
  slotlock_book_pool: 'write',
548
567
  slotlock_get_pool_availability: 'read',
@@ -614,6 +633,14 @@ const SELECTING_OPERATIONS = new Map([
614
633
  ['slotlock_find_available_slots', 20],
615
634
  ]);
616
635
  const SELECTION_FIELDS = ['location', 'group', 'labels', 'kind', 'pool'];
636
+ /** Searches that take the resources' usual length when they name none. */
637
+ const DEFAULT_LENGTH_OPERATIONS = new Set([
638
+ 'slotlock_find_next_available',
639
+ 'slotlock_find_available_slots',
640
+ ]);
641
+ /** The most ranges one search answers. */
642
+ const MAX_LISTED_RANGES = 50;
643
+ const MAX_RANGE_RESOURCES = 100;
617
644
  /** Whether a search's input names its resources, or selects them, and not both or neither. */
618
645
  function selectsOrNames(input) {
619
646
  const selects = SELECTION_FIELDS.some((field) => input[field] !== undefined);
@@ -656,6 +683,8 @@ const EVENT_CREATE_INPUT = z
656
683
  recurrence_exceptions: z.array(RECURRENCE_EXCEPTION_INPUT).max(500).optional(),
657
684
  /** Book the time one of your holds keeps (slotlock_hold_slots); its alternatives are released. */
658
685
  hold_id: HOLD_ID.optional(),
686
+ /** False: no one should be notified about it, nor reminded (RFC 0004 P3). Default true. */
687
+ notify: z.boolean().optional(),
659
688
  })
660
689
  .strict()
661
690
  .refine((value) => boundedEventSpan(value.starts_at, value.ends_at));
@@ -663,10 +692,11 @@ const OPERATION_DEFINITIONS = [
663
692
  {
664
693
  name: 'slotlock_list_resources',
665
694
  title: 'List calendar resources',
666
- description: '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.',
695
+ description: '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.',
667
696
  input: z
668
697
  .object({
669
698
  cursor: z.string().max(500).optional(),
699
+ query: z.string().trim().min(1).max(SLOTLOCK_AGENT_RESOURCE_SEARCH_MAX).optional(),
670
700
  limit: z.number().int().min(1).max(100).default(50),
671
701
  pool: POOL.optional(),
672
702
  kind: RESOURCE_KIND.optional(),
@@ -758,7 +788,8 @@ const OPERATION_DEFINITIONS = [
758
788
  pool: POOL.optional(),
759
789
  start: DATE_TIME,
760
790
  end: DATE_TIME,
761
- duration_minutes: z.number().int().min(1).max(MAX_SLOT_MINUTES),
791
+ /** Left out: the resources' usual length (their booking rules' default_duration_minutes). */
792
+ duration_minutes: z.number().int().min(1).max(MAX_SLOT_MINUTES).optional(),
762
793
  })
763
794
  .strict()
764
795
  .refine(selectsOrNames, 'Give resource_ids, or select resources by location, group, labels, kind or pool: one or the other')
@@ -791,7 +822,8 @@ const OPERATION_DEFINITIONS = [
791
822
  pool: POOL.optional(),
792
823
  start: DATE_TIME,
793
824
  end: DATE_TIME,
794
- duration_minutes: z.number().int().min(1).max(MAX_SLOT_MINUTES),
825
+ /** Left out: the resources' usual length (their booking rules' default_duration_minutes). */
826
+ duration_minutes: z.number().int().min(1).max(MAX_SLOT_MINUTES).optional(),
795
827
  limit: z.number().int().min(1).max(MAX_LISTED_SLOTS).default(10),
796
828
  require_free: z.number().int().min(1).max(MAX_SLOT_RESOURCES).optional(),
797
829
  })
@@ -819,6 +851,54 @@ const OPERATION_DEFINITIONS = [
819
851
  invoke: findAvailableSlots,
820
852
  example: { resource_ids: [EXAMPLE_RESOURCE_ID], ...EXAMPLE_WINDOW, duration_minutes: 60, limit: 10 },
821
853
  },
854
+ {
855
+ name: 'slotlock_find_available_ranges',
856
+ title: 'Find available ranges',
857
+ description: '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.',
858
+ input: z
859
+ .object({
860
+ resource_ids: z.array(RESOURCE_ID).min(1).max(MAX_SLOT_RESOURCES).optional(),
861
+ pool: POOL.optional(),
862
+ pickup_start: DATE_TIME,
863
+ pickup_end: DATE_TIME,
864
+ days: z.number().int().min(1).max(366).optional(),
865
+ return_start: DATE_TIME.optional(),
866
+ return_end: DATE_TIME.optional(),
867
+ limit: z.number().int().min(1).max(MAX_LISTED_RANGES).default(10),
868
+ })
869
+ .strict()
870
+ .refine((value) => (value.resource_ids !== undefined) !== (value.pool !== undefined), 'Give resource_ids or pool, one of them')
871
+ .refine((value) => value.days !== undefined
872
+ ? value.return_start === undefined && value.return_end === undefined
873
+ : value.return_start !== undefined && value.return_end !== undefined, 'Give days, or return_start and return_end')
874
+ .refine((value) => {
875
+ const pickupStart = Date.parse(value.pickup_start);
876
+ const pickupEnd = Date.parse(value.pickup_end);
877
+ const last = value.days !== undefined ? pickupEnd + (value.days + 1) * 86_400_000 : Date.parse(value.return_end ?? '');
878
+ const returnStart = value.return_start === undefined ? pickupStart : Date.parse(value.return_start);
879
+ return pickupEnd >= pickupStart && last >= returnStart && last - pickupStart <= MAX_AGENT_WINDOW_MS;
880
+ }, 'The windows must run forward, within the longest window a search reads'),
881
+ output: z
882
+ .object({
883
+ ranges: z
884
+ .array(z.object({ resource_id: RESOURCE_ID, start: DATE_TIME, end: DATE_TIME }).strict())
885
+ .max(MAX_LISTED_RANGES),
886
+ coverage: COVERAGE,
887
+ /** The pool's members searched, when the call named a pool. */
888
+ selected_resource_ids: z.array(RESOURCE_ID).max(MAX_RANGE_RESOURCES).optional(),
889
+ })
890
+ .strict(),
891
+ risk: { readOnly: true, destructive: false, idempotent: true },
892
+ invoke: findAvailableRanges,
893
+ example: {
894
+ pool: 'compact-car',
895
+ pickup_start: '2027-03-05T09:00:00Z',
896
+ pickup_end: '2027-03-05T12:00:00Z',
897
+ return_start: '2027-03-08T09:00:00Z',
898
+ return_end: '2027-03-08T12:00:00Z',
899
+ limit: 5,
900
+ },
901
+ },
822
902
  {
823
903
  name: 'slotlock_create_event',
824
904
  title: 'Create resource event',
@@ -890,7 +970,7 @@ const OPERATION_DEFINITIONS = [
890
970
  {
891
971
  name: 'slotlock_book_together',
892
972
  title: 'Book resources together',
893
- description: '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.',
973
+ description: '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.',
894
974
  input: z
895
975
  .object({
896
976
  resource_ids: z.array(RESOURCE_ID).min(2).max(MAX_SLOT_RESOURCES),
@@ -898,6 +978,8 @@ const OPERATION_DEFINITIONS = [
898
978
  ends_at: DATE_TIME,
899
979
  timezone: z.string().min(1).max(100),
900
980
  title: z.string().trim().min(1).max(500).optional(),
981
+ /** The set slotlock_hold_together held, booked from its holds (RFC 0004 B7). */
982
+ hold_group_id: z.string().uuid().optional(),
901
983
  idempotency_key: IDEMPOTENCY_KEY,
902
984
  })
903
985
  .strict()
@@ -1032,7 +1114,7 @@ const OPERATION_DEFINITIONS = [
1032
1114
  {
1033
1115
  name: 'slotlock_hold_slots',
1034
1116
  title: 'Hold candidate slots',
1035
- description: '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.',
1117
+ description: '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.',
1036
1118
  input: z
1037
1119
  .object({
1038
1120
  slots: z
@@ -1044,8 +1126,19 @@ const OPERATION_DEFINITIONS = [
1044
1126
  .max(5),
1045
1127
  ttl_seconds: z.number().int().min(30).max(900).default(300),
1046
1128
  idempotency_key: IDEMPOTENCY_KEY,
1129
+ /** Booked only once the integrator records its payment (RFC 0004 C3). */
1130
+ payment_required: z.boolean().optional(),
1131
+ /** What that payment must be, in the currency's minor unit. */
1132
+ payment: z
1133
+ .object({ amount_minor: z.number().int().min(0).max(1_000_000_000_000), currency: z.string().regex(/^[A-Z]{3}$/) })
1134
+ .strict()
1135
+ .optional(),
1047
1136
  })
1048
- .strict(),
1137
+ .strict()
1138
+ .refine((value) => value.payment === undefined || value.payment_required === true, {
1139
+ message: 'payment needs payment_required',
1140
+ path: ['payment'],
1141
+ }),
1049
1142
  output: z
1050
1143
  .object({
1051
1144
  group_id: HOLD_ID,
@@ -1057,6 +1150,12 @@ const OPERATION_DEFINITIONS = [
1057
1150
  start: DATE_TIME,
1058
1151
  end: DATE_TIME,
1059
1152
  expires_at: DATE_TIME,
1153
+ payment_required: z.literal(true).optional(),
1154
+ payment_amount: z
1155
+ .object({ amount_minor: z.number().int().min(0), currency: z.string().regex(/^[A-Z]{3}$/) })
1156
+ .strict()
1157
+ .nullable()
1158
+ .optional(),
1060
1159
  })
1061
1160
  .strict())
1062
1161
  .max(5),
@@ -1083,6 +1182,42 @@ const OPERATION_DEFINITIONS = [
1083
1182
  idempotency_key: 'hold-handover-2027-03-02',
1084
1183
  },
1085
1184
  },
1185
+ {
1186
+ name: 'slotlock_hold_together',
1187
+ title: 'Hold resources together',
1188
+ description: '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.',
1189
+ input: z
1190
+ .object({
1191
+ resource_ids: z.array(RESOURCE_ID).min(2).max(MAX_SLOT_RESOURCES),
1192
+ start: DATE_TIME,
1193
+ end: DATE_TIME,
1194
+ ttl_seconds: z.number().int().min(30).max(900).default(300),
1195
+ idempotency_key: IDEMPOTENCY_KEY,
1196
+ })
1197
+ .strict()
1198
+ .refine(boundedAgentWindow),
1199
+ output: z
1200
+ .object({
1201
+ group_id: z.string().uuid(),
1202
+ holds: z
1203
+ .array(z
1204
+ .object({ hold_id: z.string().uuid(), resource_id: RESOURCE_ID, start: DATE_TIME, end: DATE_TIME, expires_at: DATE_TIME })
1205
+ .strict())
1206
+ .min(2)
1207
+ .max(MAX_SLOT_RESOURCES),
1208
+ expires_at: DATE_TIME,
1209
+ })
1210
+ .strict(),
1211
+ risk: { readOnly: false, destructive: false, idempotent: true },
1212
+ invoke: holdTogether,
1213
+ example: {
1214
+ resource_ids: [EXAMPLE_RESOURCE_ID, '3c2d1e0f-9a8b-4c7d-8e6f-5a4b3c2d1e0f'],
1215
+ start: '2027-03-02T09:00:00Z',
1216
+ end: '2027-03-02T17:00:00Z',
1217
+ ttl_seconds: 300,
1218
+ idempotency_key: 'hold-van-and-driver-1',
1219
+ },
1220
+ },
1086
1221
  {
1087
1222
  name: 'slotlock_release_slots',
1088
1223
  title: 'Release held slots',
@@ -1633,6 +1768,15 @@ export async function invokeSlotlockAgentOperation(args) {
1633
1768
  input = Object.fromEntries(Object.entries(input).filter(([key]) => !SELECTION_FIELDS.includes(key)));
1634
1769
  input.resource_ids = selected;
1635
1770
  }
1771
+ // A search that names no length takes the resources' usual one (RFC 0004 B3).
1772
+ if (DEFAULT_LENGTH_OPERATIONS.has(operation.name) && input.duration_minutes === undefined) {
1773
+ const usual = backend.defaultDuration
1774
+ ? await backend.defaultDuration(context, (input.resource_ids ?? []))
1775
+ : null;
1776
+ if (usual === null)
1777
+ return { ok: false, status: 400, code: 'duration_required' };
1778
+ input = { ...input, duration_minutes: usual };
1779
+ }
1636
1780
  const asked = input;
1637
1781
  recover = (code) => alternativesForRefusal(backend, context, operation.name, asked, code);
1638
1782
  let answered = await method.call(backend, context, input);
@@ -1990,6 +2134,7 @@ function parseSubscriptionFilter(value) {
1990
2134
  /** Natural-language guidance `server/discover` gives the model on how to use these tools well. */
1991
2135
  const SLOTLOCK_MCP_INSTRUCTIONS = [
1992
2136
  'Slotlock is a tenant-isolated resource calendar (vehicles, rooms, equipment).',
2137
+ 'A resource is named in calls by its id (resource_id, resource_ids), never by its name or reference: find the id of one a person names with slotlock_list_resources and query (what they call it; typos and accents are forgiven). resource_not_found for something that is not an id names, in candidate_1 to candidate_3, the resources that best match it. A kind, label or location filter leaves out resources that do not state it, so search without one before saying a resource does not exist, and ask the person when several match equally.',
1993
2138
  'Check availability before booking: slotlock_get_free_busy returns busy intervals and slotlock_find_next_available the earliest free slot of a given length, and slotlock_find_available_slots up to 50 of them to offer a person a choice; an answer is certain only when coverage.certainty is "certain".',
1994
2139
  'slotlock_create_event needs an idempotency_key: retry with the same key and arguments, never a new key for the same booking. slotlock_update_event and slotlock_delete_event need the event revision you last read as expected_revision.',
1995
2140
  'Times are RFC 3339 with an offset; intervals are half-open [start, end).',
@@ -39,7 +39,6 @@ export declare function slotlockAgentErrorStatus(code: string): 400 | 404 | 409
39
39
  * store's client, used for the feed's head.
40
40
  */
41
41
  export declare function createSlotlockStoreChangeSource(store: SlotlockStore, sql: Sql): SlotlockAgentChangeSource;
42
- /** Bind the protocol server to the production store without giving a request control of tenancy. */
43
42
  export declare function createSlotlockStoreAgentBackend(store: SlotlockStore, options: SlotlockStoreAgentBackendOptions): SlotlockAgentCalendarBackend;
44
43
  export interface SlotlockStoreApplicationBackendOptions {
45
44
  /**
@@ -66,6 +65,12 @@ export interface SlotlockStoreApplicationBackendOptions {
66
65
  * approvals_unavailable; declining still works.
67
66
  */
68
67
  agentBackend?: SlotlockAgentCalendarBackend;
68
+ /**
69
+ * How `slotlock_import_holidays` fetches a calendar's url (RFC 0004 B1): give it a fetch that
70
+ * refuses private addresses (`createSlotlockGuardedFetch`). Unset: a url answers 400
71
+ * holiday_url_not_served and only the calendar's text is read.
72
+ */
73
+ holidayFetch?: typeof fetch;
69
74
  }
70
75
  /**
71
76
  * Run a granted approval's write as the agent that asked for it, in one transaction with the grant: