slotlock 0.6.2 → 0.7.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 (100) hide show
  1. package/CHANGELOG.md +492 -0
  2. package/CONTRIBUTING.md +12 -5
  3. package/README.md +50 -1544
  4. package/SECURITY.md +5 -2
  5. package/SPEC.md +1 -1
  6. package/agent-server.d.ts +160 -8
  7. package/agent-server.js +560 -49
  8. package/agent-store-backend.d.ts +39 -7
  9. package/agent-store-backend.js +1439 -104
  10. package/api-keys.d.ts +69 -3
  11. package/api-keys.js +218 -29
  12. package/application-api.d.ts +59 -6
  13. package/application-api.js +842 -39
  14. package/approvals.d.ts +88 -0
  15. package/approvals.js +293 -0
  16. package/assistant.d.ts +58 -27
  17. package/assistant.js +151 -53
  18. package/billing.d.ts +114 -0
  19. package/billing.js +258 -0
  20. package/booking-rules.d.ts +30 -41
  21. package/booking-rules.js +17 -25
  22. package/change-watch.d.ts +1 -0
  23. package/change-watch.js +1 -1
  24. package/cli.js +3 -4
  25. package/config.d.ts +42 -6
  26. package/config.js +114 -7
  27. package/dashboard-icon.d.ts +1 -0
  28. package/dashboard-icon.js +5 -0
  29. package/dashboard.d.ts +203 -7
  30. package/dashboard.js +1558 -64
  31. package/ddl.d.ts +67 -35
  32. package/ddl.js +2732 -162
  33. package/engine.d.ts +6 -9
  34. package/engine.js +5 -9
  35. package/event-feed.d.ts +53 -35
  36. package/event-feed.js +201 -64
  37. package/event-schema.js +6 -9
  38. package/hooks.d.ts +2 -4
  39. package/hooks.js +2 -4
  40. package/ical-worker-thread.js +3 -4
  41. package/ical-worker.d.ts +8 -10
  42. package/ical-worker.js +7 -9
  43. package/index.d.ts +15 -4
  44. package/index.js +8 -2
  45. package/interop.js +1 -4
  46. package/maintenance.d.ts +24 -10
  47. package/maintenance.js +59 -28
  48. package/mcp-app.d.ts +7 -9
  49. package/mcp-app.js +7 -9
  50. package/mcp-modern.d.ts +14 -16
  51. package/mcp-modern.js +23 -27
  52. package/metrics.d.ts +41 -0
  53. package/metrics.js +22 -1
  54. package/node-server.d.ts +9 -9
  55. package/node-server.js +25 -34
  56. package/openapi.json +14675 -5957
  57. package/otel.d.ts +86 -0
  58. package/otel.js +273 -0
  59. package/overage.d.ts +33 -0
  60. package/overage.js +115 -0
  61. package/package-version.js +2 -4
  62. package/package.json +1 -1
  63. package/partner-billing.d.ts +81 -0
  64. package/partner-billing.js +333 -0
  65. package/partner-names.d.ts +8 -0
  66. package/partner-names.js +10 -0
  67. package/partners.d.ts +147 -0
  68. package/partners.js +375 -0
  69. package/plans.d.ts +92 -0
  70. package/plans.js +115 -0
  71. package/quotas.d.ts +108 -0
  72. package/quotas.js +258 -0
  73. package/rate-limit.d.ts +27 -2
  74. package/rate-limit.js +106 -23
  75. package/resource-profile.d.ts +135 -0
  76. package/resource-profile.js +263 -0
  77. package/rest-api.d.ts +15 -14
  78. package/rest-api.js +79 -15
  79. package/rules.d.ts +6 -7
  80. package/rules.js +30 -42
  81. package/sandbox.d.ts +70 -0
  82. package/sandbox.js +295 -0
  83. package/self-host.d.ts +46 -4
  84. package/self-host.js +990 -41
  85. package/slotlock.manifest.json +1 -1
  86. package/store.d.ts +482 -165
  87. package/store.js +1460 -484
  88. package/sync.d.ts +44 -59
  89. package/sync.js +325 -469
  90. package/teams.d.ts +25 -0
  91. package/teams.js +63 -0
  92. package/timezone.d.ts +25 -33
  93. package/timezone.js +50 -53
  94. package/types.d.ts +130 -57
  95. package/usage.d.ts +106 -0
  96. package/usage.js +238 -0
  97. package/webhooks.d.ts +22 -25
  98. package/webhooks.js +35 -40
  99. package/weekly-hours.d.ts +3 -0
  100. package/weekly-hours.js +1 -1
package/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /// <reference types="node" preserve="true" />
2
2
  export { findNextAvailable, mergeIntervals, subtractBusy } from './engine.js';
3
3
  export { type SlotlockBookingRule, type SlotlockBookingRules, type SlotlockBookingSearch, bookingRuleBroken, bookingSearch, bookingSearchBlocked, bookingSearchReach, nextStepStart, parseBookingRuleDefaults, parseBookingRules, } from './booking-rules.js';
4
+ export { SLOTLOCK_GROUPING_REF, SLOTLOCK_MAX_GROUPS_PER_RESOURCE, SLOTLOCK_MAX_LOCATION_DEPTH, SLOTLOCK_MAX_LOCATIONS, SLOTLOCK_MAX_RESOURCE_GROUPS, SLOTLOCK_RESOURCE_KINDS, SLOTLOCK_RESOURCE_LABEL, SLOTLOCK_RESOURCE_PROFILE_FIELDS, SLOTLOCK_RESOURCE_PROFILE_LIMITS, parseSlotlockLocation, parseSlotlockResourceGroup, parseSlotlockResourceProfile, type SlotlockLocation, type SlotlockResourceGroup, type SlotlockResourceAttributes, type SlotlockResourceKind, type SlotlockResourceProfileChange, type SlotlockResourceProfileInput, } from './resource-profile.js';
4
5
  export { type CreateWebhookEndpointResult, type ListChangesResult, SLOTLOCK_CHANGE_ORIGINS, SLOTLOCK_CHANGE_RETENTION_DAYS, SLOTLOCK_CHANGE_TYPES, SLOTLOCK_MAX_WEBHOOK_ENDPOINTS, SLOTLOCK_WEBHOOK_FAILING_LIMIT_MS, slotlockChangeFeedHead, type SlotlockChange, type SlotlockChangeOrigin, type SlotlockChangeType, type SlotlockEventFeed, type SlotlockWebhookClaim, type SlotlockWebhookEndpoint, slotlockCoverageSourceId, } from './event-feed.js';
5
6
  export { type SlotlockCloudEvent, type SlotlockWebhookPassResult, type SlotlockWebhookResponse, type SlotlockWebhookScheduler, type SlotlockWebhookSender, createSlotlockGuardedFetch, createSlotlockWebhookSender, deliverSlotlockWebhooks, isPrivateAddress, signSlotlockWebhook, slotlockCloudEvent, slotlockWebhookRetryDelayMs, startSlotlockWebhookDelivery, } from './webhooks.js';
6
7
  export { type SlotlockMetrics, createSlotlockMetrics } from './metrics.js';
@@ -11,8 +12,8 @@ export { emitVAvailability, parseVAvailability, recurrenceRuleFromRRule, SLOTLOC
11
12
  export type { EmitVAvailabilityOptions, JSCalendarAlert, JSCalendarEvent, JSCalendarParticipant, JSCalendarRecurrenceRule, ParsedVAvailability, } from './interop.js';
12
13
  export { createTimeZoneConverter, zonedCalendarDate, zonedDateTimeToUtc, zonedWallTime, } from './timezone.js';
13
14
  export type { LocalDateTime, TimeZoneConverter, ZonedCalendarDate, ZonedWallTime, } from './timezone.js';
14
- export { createSlotlockAgentServer, clientSupportsSlotlockMcpApp, invokeSlotlockAgentOperation, isSlotlockAgentOperation, slotlockAgentTools, slotlockApplicationOperations, slotlockOpenApi, slotlockCalendarResourceUri, slotlockMcpToolResult, parseSlotlockA2ATimestamp, resolveSlotlockAgentOperation, slotlockAgentOperationScope, SLOTLOCK_A2A_PROTOCOL_VERSION, SLOTLOCK_A2A_TASK_PAGE_SIZE, SLOTLOCK_A2A_WATCH_SKILL, SLOTLOCK_A2A_TASK_STATES, SLOTLOCK_AGENT_OPERATION_LEGACY_NAMES, SLOTLOCK_AGENT_SERVER_VERSION, SLOTLOCK_MCP_LEGACY_PROTOCOL_VERSION, SLOTLOCK_MCP_PROTOCOL_VERSION, SLOTLOCK_MCP_RATE_LIMITED_ERROR_CODE, SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE, SLOTLOCK_MCP_SUPPORTED_PROTOCOL_VERSIONS, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, SlotlockAgentOperationError, } from './agent-server.js';
15
- export type { SlotlockAgentCalendarBackend, SlotlockAgentConfirmationOptions, SlotlockAgentErrorContext, SlotlockAgentInvocationContext, SlotlockAgentOperation, SlotlockAgentOperationDispatchOptions, SlotlockAgentOperationDispatchResult, SlotlockAgentPrincipal, SlotlockAgentScope, SlotlockAgentServerEvent, SlotlockAgentServerOAuthOptions, SlotlockAgentServerOptions, SlotlockAgentSubscriptionOptions, SlotlockAgentWriteOperation, SlotlockArgumentIssue, SlotlockMcpToolResult, SlotlockTraceContext, } from './agent-server.js';
15
+ export { createSlotlockAgentServer, clientSupportsSlotlockMcpApp, invokeSlotlockAgentOperation, isSlotlockAgentOperation, slotlockAgentTools, slotlockApplicationOperations, slotlockOpenApi, slotlockCalendarResourceUri, slotlockMcpToolResult, parseSlotlockA2ATimestamp, resolveSlotlockAgentOperation, slotlockAgentOperationScope, SLOTLOCK_A2A_PROTOCOL_VERSION, SLOTLOCK_A2A_TASK_PAGE_SIZE, SLOTLOCK_A2A_WATCH_SKILL, SLOTLOCK_A2A_TASK_STATES, SLOTLOCK_AGENT_OPERATION_LEGACY_NAMES, SLOTLOCK_AGENT_SERVER_VERSION, SLOTLOCK_MAX_ALTERNATIVES, SLOTLOCK_MCP_LEGACY_PROTOCOL_VERSION, SLOTLOCK_MCP_PROTOCOL_VERSION, SLOTLOCK_MCP_RATE_LIMITED_ERROR_CODE, SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE, SLOTLOCK_MCP_SUPPORTED_PROTOCOL_VERSIONS, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, SlotlockAgentOperationError, } from './agent-server.js';
16
+ export type { SlotlockAgentCalendarBackend, SlotlockAgentConfirmationOptions, SlotlockAgentErrorContext, SlotlockAgentInvocationContext, SlotlockAgentOperation, SlotlockAgentOperationDispatchOptions, SlotlockAgentOperationDispatchResult, SlotlockAgentPrincipal, SlotlockAgentScope, SlotlockAgentServerEvent, SlotlockAgentServerOAuthOptions, SlotlockAgentServerOptions, SlotlockAgentSubscriptionOptions, SlotlockAgentWriteOperation, SlotlockAlternative, SlotlockArgumentIssue, SlotlockMcpToolResult, SlotlockTraceContext, } from './agent-server.js';
16
17
  export { createSlotlockNodeServer } from './node-server.js';
17
18
  export type { SlotlockNodeListenOptions, SlotlockNodeServer, SlotlockNodeServerAddress, SlotlockNodeServerCloseResult, SlotlockNodeServerErrorPhase, SlotlockNodeServerFetchTarget, SlotlockNodeServerOptions, } from './node-server.js';
18
19
  export { createSlotlockStoreAgentBackend, createSlotlockStoreApplicationBackend, createSlotlockStoreChangeSource, projectSlotlockCalendarEvent, } from './agent-store-backend.js';
@@ -22,11 +23,21 @@ export { SLOTLOCK_APPLICATION_OPERATIONS, SLOTLOCK_APPLICATION_SCOPE, isSlotlock
22
23
  export type { SlotlockApplicationBackend, SlotlockApplicationInvocationContext, SlotlockApplicationOperation, SlotlockApplicationOperationDefinition, } from './application-api.js';
23
24
  export { maintainSlotlockTenant, startSlotlockMaintenance, withSlotlockMaintenanceLock, } from './maintenance.js';
24
25
  export type { SlotlockMaintenanceResult, SlotlockMaintenanceScheduler, SlotlockMaintenanceSchedulerOptions, } from './maintenance.js';
25
- export { createSlotlockApiKeyAuthenticator, createSlotlockApiKeyStore, generateSlotlockApiKey, isSlotlockApiKey, slotlockApiKeyDigest, SLOTLOCK_API_KEY_ACTIVE_LIMIT, SLOTLOCK_API_KEY_MAX_LIFETIME_DAYS, SLOTLOCK_API_KEY_PREFIX, SLOTLOCK_API_KEY_RETAINED_LIMIT, SLOTLOCK_API_KEY_SCOPES, } from './api-keys.js';
26
+ export { createSlotlockApiKeyAuthenticator, createSlotlockApiKeyStore, generateSlotlockApiKey, isSlotlockApiKey, isSlotlockTestTenant, slotlockLiveTenant, SLOTLOCK_TEST_API_KEY_PREFIX, SLOTLOCK_TEST_TENANT_SUFFIX, slotlockApiKeyDigest, SLOTLOCK_API_KEY_ACTIVE_LIMIT, SLOTLOCK_API_KEY_MAX_LIFETIME_DAYS, SLOTLOCK_API_KEY_PREFIX, SLOTLOCK_API_KEY_RETAINED_LIMIT, SLOTLOCK_API_KEY_SCOPES, } from './api-keys.js';
26
27
  export { createSlotlockDashboard, createSlotlockDashboardResources, createSlotlockAcceptanceRecords, createSlotlockDashboardState, createSlotlockMemoryDashboardState, SLOTLOCK_DASHBOARD_MAX_RESOURCES, } from './dashboard.js';
27
28
  export type { SlotlockDashboard, SlotlockDashboardAssistantOptions, SlotlockDashboardOptions, SlotlockDashboardResources, SlotlockDashboardState, } from './dashboard.js';
28
29
  export { createSlotlockAssistantStore, createSlotlockMemoryAssistantStore, SLOTLOCK_ASSISTANT_TOOL_USE, } from './assistant.js';
29
30
  export type { SlotlockAssistantStore } from './assistant.js';
31
+ export { createSlotlockPartnerKeyAuthenticator, createSlotlockPartnerStore, parseSlotlockPartnerContract, slotlockPartnerTenantRef, SLOTLOCK_PARTNER_ACCOUNT, SLOTLOCK_PARTNER_ID, SLOTLOCK_PARTNER_SCOPE, } from './partners.js';
32
+ export { createSlotlockDashboardTeams, createSlotlockOrganizationCheck } from './teams.js';
33
+ export { reportSlotlockOverage, SLOTLOCK_DEFAULT_OVERAGE_STOP_PERCENT } from './overage.js';
34
+ export type { SlotlockOverageReport } from './overage.js';
35
+ export { createSlotlockOtlpExporter, readSlotlockOtlpConfig, slotlockCallSpan } from './otel.js';
36
+ export type { SlotlockOtlpConfig, SlotlockTraceExporter } from './otel.js';
37
+ export type { SlotlockDashboardTeams } from './teams.js';
38
+ export { closeSlotlockPartnerMonth, priceSlotlockPartnerMonth, slotlockClientLabel, slotlockPartnerMonthChecksum, } from './partner-billing.js';
39
+ export type { SlotlockPartnerAccountMonth, SlotlockPartnerInvoiceLine, SlotlockPartnerMonthClose, } from './partner-billing.js';
40
+ export type { SlotlockPartnerAccount, SlotlockPartnerContract, SlotlockPartnerPlan, SlotlockPartnerPricing, SlotlockPartnerStore, } from './partners.js';
30
41
  export type { CreateSlotlockApiKeyInput, SlotlockApiKey, SlotlockApiKeyAuthenticatorOptions, SlotlockApiKeyChange, SlotlockApiKeyIdentity, SlotlockApiKeyScope, SlotlockApiKeyStore, } from './api-keys.js';
31
42
  export { ICalendarWorkerBusyError, ICalendarWorkerPoolClosedError, createICalendarWorkerPool, } from './ical-worker.js';
32
43
  export type { ICalendarWorkerPool, ICalendarWorkerPoolOptions } from './ical-worker.js';
@@ -34,4 +45,4 @@ export { CalendarContractError, CalendarExpansionLimitError, emitICalendar, emit
34
45
  export type { ExternalCalendarChange, ExternalCalendarChangeInput, ExternalCalendarStatus, ICalendarEvent, ICalendarExpansionWindow, ICalendarFeedRead, ICalendarSeriesRefusal, ITipMethod, } from './sync.js';
35
46
  export { createSlotlockApplicationRoleGrantsDdl, createSlotlockTenantRlsDdl, SLOTLOCK_API_KEY_FUNCTIONS, SLOTLOCK_API_KEY_TABLES, SLOTLOCK_CORE_DDL, SLOTLOCK_DASHBOARD_FUNCTIONS, SLOTLOCK_DASHBOARD_TABLES, SLOTLOCK_DEFINER_FUNCTIONS, SLOTLOCK_DEFINER_TABLES, SLOTLOCK_OPERATOR_FUNCTIONS, SLOTLOCK_SCHEMA_FUNCTIONS, SLOTLOCK_SCHEMA_TABLES, SLOTLOCK_SCHEMA_VERSION, SLOTLOCK_TENANT_ADMIN_FUNCTIONS, SLOTLOCK_TENANT_ADMIN_TABLES, SLOTLOCK_TENANT_CONTEXT_SETTING, SLOTLOCK_TENANT_RLS_DDL, SLOTLOCK_TENANT_TABLES, } from './ddl.js';
36
47
  export { assertSlotlockSchemaCurrent, calendarEventRollingHorizon, createSlotlockStore, readSlotlockSchemaVersion, SLOTLOCK_AGENT_OWNER_COMMAND_QUOTA, SLOTLOCK_AGENT_OWNER_EVENT_QUOTA, SLOTLOCK_CALENDAR_HORIZON_DAYS, SLOTLOCK_EVENT_COMMAND_RETENTION_DAYS, SLOTLOCK_INTERNAL_EVENT_OWNER_REF, SLOTLOCK_LOCAL_RECURRENCE_COVERAGE_SOURCE, SLOTLOCK_MATERIALIZATION_WINDOW_DAYS, SLOTLOCK_MAX_EVENT_DURATION_DAYS, SLOTLOCK_TENANT_WRITE_METHODS, type SlotlockHoldTenantOptions, type SlotlockMigrationOptions, type SlotlockSql, type SlotlockStore, type SlotlockStoreOptions, type SlotlockTenantLimits, type SlotlockTenantStore, } from './store.js';
37
- export type { AcquireHoldResult, BookingRetentionResult, CalendarAttendee, CalendarAttendeeRole, CalendarCoverageAssessment, CalendarCoverageCommand, CalendarCoverageResult, CalendarEventContent, CalendarEventOccurrence, CalendarEventRetentionResult, CalendarEventStatus, CalendarFreeBusy, CalendarHorizonRollCursor, CalendarHorizonRollResult, CalendarOrganizer, CalendarParticipationStatus, CalendarRecurrence, CalendarRecurrenceException, CalendarReminder, CalendarTransparency, ConfirmHoldResult, CancelCalendarEventResult, CancelExternalReservationResult, CreateReservationResult, DeleteResourceResult, ExternalReservationCommand, ExternalReservationResult, Interval, SlotlockAgendaEntry, SlotlockHold, SlotlockCalendarEvent, SlotlockReservation, SlotlockResource, SlotlockTenantErasure, SlotlockBookingRuleDefaults, PutCalendarEventCommand, PutCalendarEventResult, ReleaseHoldResult, TrustedICalendarEvent, WeeklyAvailabilityRule, } from './types.js';
48
+ export type { AcquireHoldResult, BookingRetentionResult, CalendarAttendee, CalendarAttendeeRole, CalendarCoverageAssessment, CalendarCoverageCommand, CalendarCoverageResult, CalendarEventContent, CalendarEventOccurrence, CalendarEventRetentionResult, CalendarEventStatus, CalendarFreeBusy, CalendarHorizonRollCursor, CalendarHorizonRollResult, CalendarOrganizer, CalendarParticipationStatus, CalendarRecurrence, CalendarRecurrenceException, CalendarReminder, CalendarTransparency, ConfirmHoldResult, CancelCalendarEventResult, CancelExternalReservationResult, CreateReservationResult, DeleteResourceResult, ExternalReservationCommand, ExternalReservationResult, Interval, SlotlockAgendaEntry, SlotlockHold, SlotlockCalendarEvent, SlotlockBookingDescription, SlotlockReservation, SlotlockResource, SlotlockTenantErasure, SlotlockBookingRuleDefaults, PutCalendarEventCommand, PutCalendarEventResult, ReleaseHoldResult, TrustedICalendarEvent, WeeklyAvailabilityRule, } from './types.js';
package/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  /// <reference types="node" preserve="true" />
6
6
  export { findNextAvailable, mergeIntervals, subtractBusy } from './engine.js';
7
7
  export { bookingRuleBroken, bookingSearch, bookingSearchBlocked, bookingSearchReach, nextStepStart, parseBookingRuleDefaults, parseBookingRules, } from './booking-rules.js';
8
+ export { SLOTLOCK_GROUPING_REF, SLOTLOCK_MAX_GROUPS_PER_RESOURCE, SLOTLOCK_MAX_LOCATION_DEPTH, SLOTLOCK_MAX_LOCATIONS, SLOTLOCK_MAX_RESOURCE_GROUPS, SLOTLOCK_RESOURCE_KINDS, SLOTLOCK_RESOURCE_LABEL, SLOTLOCK_RESOURCE_PROFILE_FIELDS, SLOTLOCK_RESOURCE_PROFILE_LIMITS, parseSlotlockLocation, parseSlotlockResourceGroup, parseSlotlockResourceProfile, } from './resource-profile.js';
8
9
  export { SLOTLOCK_CHANGE_ORIGINS, SLOTLOCK_CHANGE_RETENTION_DAYS, SLOTLOCK_CHANGE_TYPES, SLOTLOCK_MAX_WEBHOOK_ENDPOINTS, SLOTLOCK_WEBHOOK_FAILING_LIMIT_MS, slotlockChangeFeedHead, slotlockCoverageSourceId, } from './event-feed.js';
9
10
  export { createSlotlockGuardedFetch, createSlotlockWebhookSender, deliverSlotlockWebhooks, isPrivateAddress, signSlotlockWebhook, slotlockCloudEvent, slotlockWebhookRetryDelayMs, startSlotlockWebhookDelivery, } from './webhooks.js';
10
11
  export { createSlotlockMetrics } from './metrics.js';
@@ -12,15 +13,20 @@ export { expandRules } from './rules.js';
12
13
  export { availabilityRuleProblem, parseAvailabilityRules, rulesToWeeklyHours, SLOTLOCK_MAX_AVAILABILITY_RULES, weeklyHoursToRules, } from './weekly-hours.js';
13
14
  export { emitVAvailability, parseVAvailability, recurrenceRuleFromRRule, SLOTLOCK_MAX_VAVAILABILITY_CHARACTERS, toJSCalendarEvent, } from './interop.js';
14
15
  export { createTimeZoneConverter, zonedCalendarDate, zonedDateTimeToUtc, zonedWallTime, } from './timezone.js';
15
- export { createSlotlockAgentServer, clientSupportsSlotlockMcpApp, invokeSlotlockAgentOperation, isSlotlockAgentOperation, slotlockAgentTools, slotlockApplicationOperations, slotlockOpenApi, slotlockCalendarResourceUri, slotlockMcpToolResult, parseSlotlockA2ATimestamp, resolveSlotlockAgentOperation, slotlockAgentOperationScope, SLOTLOCK_A2A_PROTOCOL_VERSION, SLOTLOCK_A2A_TASK_PAGE_SIZE, SLOTLOCK_A2A_WATCH_SKILL, SLOTLOCK_A2A_TASK_STATES, SLOTLOCK_AGENT_OPERATION_LEGACY_NAMES, SLOTLOCK_AGENT_SERVER_VERSION, SLOTLOCK_MCP_LEGACY_PROTOCOL_VERSION, SLOTLOCK_MCP_PROTOCOL_VERSION, SLOTLOCK_MCP_RATE_LIMITED_ERROR_CODE, SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE, SLOTLOCK_MCP_SUPPORTED_PROTOCOL_VERSIONS, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, SlotlockAgentOperationError, } from './agent-server.js';
16
+ export { createSlotlockAgentServer, clientSupportsSlotlockMcpApp, invokeSlotlockAgentOperation, isSlotlockAgentOperation, slotlockAgentTools, slotlockApplicationOperations, slotlockOpenApi, slotlockCalendarResourceUri, slotlockMcpToolResult, parseSlotlockA2ATimestamp, resolveSlotlockAgentOperation, slotlockAgentOperationScope, SLOTLOCK_A2A_PROTOCOL_VERSION, SLOTLOCK_A2A_TASK_PAGE_SIZE, SLOTLOCK_A2A_WATCH_SKILL, SLOTLOCK_A2A_TASK_STATES, SLOTLOCK_AGENT_OPERATION_LEGACY_NAMES, SLOTLOCK_AGENT_SERVER_VERSION, SLOTLOCK_MAX_ALTERNATIVES, SLOTLOCK_MCP_LEGACY_PROTOCOL_VERSION, SLOTLOCK_MCP_PROTOCOL_VERSION, SLOTLOCK_MCP_RATE_LIMITED_ERROR_CODE, SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE, SLOTLOCK_MCP_SUPPORTED_PROTOCOL_VERSIONS, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, SlotlockAgentOperationError, } from './agent-server.js';
16
17
  export { createSlotlockNodeServer } from './node-server.js';
17
18
  export { createSlotlockStoreAgentBackend, createSlotlockStoreApplicationBackend, createSlotlockStoreChangeSource, projectSlotlockCalendarEvent, } from './agent-store-backend.js';
18
19
  export { SLOTLOCK_FEED_BUSY_MS, SLOTLOCK_FEED_IDLE_MS, } from './change-watch.js';
19
20
  export { SLOTLOCK_APPLICATION_OPERATIONS, SLOTLOCK_APPLICATION_SCOPE, isSlotlockApplicationOperation, } from './application-api.js';
20
21
  export { maintainSlotlockTenant, startSlotlockMaintenance, withSlotlockMaintenanceLock, } from './maintenance.js';
21
- export { createSlotlockApiKeyAuthenticator, createSlotlockApiKeyStore, generateSlotlockApiKey, isSlotlockApiKey, slotlockApiKeyDigest, SLOTLOCK_API_KEY_ACTIVE_LIMIT, SLOTLOCK_API_KEY_MAX_LIFETIME_DAYS, SLOTLOCK_API_KEY_PREFIX, SLOTLOCK_API_KEY_RETAINED_LIMIT, SLOTLOCK_API_KEY_SCOPES, } from './api-keys.js';
22
+ export { createSlotlockApiKeyAuthenticator, createSlotlockApiKeyStore, generateSlotlockApiKey, isSlotlockApiKey, isSlotlockTestTenant, slotlockLiveTenant, SLOTLOCK_TEST_API_KEY_PREFIX, SLOTLOCK_TEST_TENANT_SUFFIX, slotlockApiKeyDigest, SLOTLOCK_API_KEY_ACTIVE_LIMIT, SLOTLOCK_API_KEY_MAX_LIFETIME_DAYS, SLOTLOCK_API_KEY_PREFIX, SLOTLOCK_API_KEY_RETAINED_LIMIT, SLOTLOCK_API_KEY_SCOPES, } from './api-keys.js';
22
23
  export { createSlotlockDashboard, createSlotlockDashboardResources, createSlotlockAcceptanceRecords, createSlotlockDashboardState, createSlotlockMemoryDashboardState, SLOTLOCK_DASHBOARD_MAX_RESOURCES, } from './dashboard.js';
23
24
  export { createSlotlockAssistantStore, createSlotlockMemoryAssistantStore, SLOTLOCK_ASSISTANT_TOOL_USE, } from './assistant.js';
25
+ export { createSlotlockPartnerKeyAuthenticator, createSlotlockPartnerStore, parseSlotlockPartnerContract, slotlockPartnerTenantRef, SLOTLOCK_PARTNER_ACCOUNT, SLOTLOCK_PARTNER_ID, SLOTLOCK_PARTNER_SCOPE, } from './partners.js';
26
+ export { createSlotlockDashboardTeams, createSlotlockOrganizationCheck } from './teams.js';
27
+ export { reportSlotlockOverage, SLOTLOCK_DEFAULT_OVERAGE_STOP_PERCENT } from './overage.js';
28
+ export { createSlotlockOtlpExporter, readSlotlockOtlpConfig, slotlockCallSpan } from './otel.js';
29
+ export { closeSlotlockPartnerMonth, priceSlotlockPartnerMonth, slotlockClientLabel, slotlockPartnerMonthChecksum, } from './partner-billing.js';
24
30
  export { ICalendarWorkerBusyError, ICalendarWorkerPoolClosedError, createICalendarWorkerPool, } from './ical-worker.js';
25
31
  export { CalendarContractError, CalendarExpansionLimitError, emitICalendar, emitITipCalendar, expandCalendarEventOccurrences, normalizeCalendarEventContent, normalizeExternalCalendarChange, parseICalendarChanges, parseICalendarFeed, parseTrustedICalendarEvents, recurrenceRuleProblem, storedCalendarEventProblem, } from './sync.js';
26
32
  export { createSlotlockApplicationRoleGrantsDdl, createSlotlockTenantRlsDdl, SLOTLOCK_API_KEY_FUNCTIONS, SLOTLOCK_API_KEY_TABLES, SLOTLOCK_CORE_DDL, SLOTLOCK_DASHBOARD_FUNCTIONS, SLOTLOCK_DASHBOARD_TABLES, SLOTLOCK_DEFINER_FUNCTIONS, SLOTLOCK_DEFINER_TABLES, SLOTLOCK_OPERATOR_FUNCTIONS, SLOTLOCK_SCHEMA_FUNCTIONS, SLOTLOCK_SCHEMA_TABLES, SLOTLOCK_SCHEMA_VERSION, SLOTLOCK_TENANT_ADMIN_FUNCTIONS, SLOTLOCK_TENANT_ADMIN_TABLES, SLOTLOCK_TENANT_CONTEXT_SETTING, SLOTLOCK_TENANT_RLS_DDL, SLOTLOCK_TENANT_TABLES, } from './ddl.js';
package/interop.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // directly.
5
5
  import ICAL from 'ical.js';
6
6
  import { zonedDateTimeFields } from './timezone.js';
7
- import { parseAvailabilityRules } from './weekly-hours.js';
7
+ import { invalidAvailability, parseAvailabilityRules } from './weekly-hours.js';
8
8
  /** The longest VAVAILABILITY text `parseVAvailability` reads. */
9
9
  export const SLOTLOCK_MAX_VAVAILABILITY_CHARACTERS = 65_536;
10
10
  const PRODID = '-//Pylota//Slotlock//EN';
@@ -13,9 +13,6 @@ const DAYS = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
13
13
  /** 2024-01-07 is a Sunday: the reference week runs Sunday 7 to Saturday 13 January. */
14
14
  const REFERENCE_SUNDAY = Date.UTC(2024, 0, 7);
15
15
  const VAVAILABILITY_DTSTAMP = '20240101T000000Z';
16
- function invalidAvailability(message) {
17
- return Object.assign(new Error(message), { code: 'invalid_availability' });
18
- }
19
16
  const pad = (value, width = 2) => String(value).padStart(width, '0');
20
17
  /** RFC 5545 §3.1: lines of at most 75 octets, continued with a leading space. */
21
18
  function fold(line) {
package/maintenance.d.ts CHANGED
@@ -8,10 +8,9 @@ export interface SlotlockMaintenanceResult {
8
8
  /** Series over the expansion budget, or whose roll failed otherwise; they keep their old coverage. */
9
9
  refused: number;
10
10
  /**
11
- * Expired holds and the answers kept for keyed ones, and past the retention window event
12
- * idempotency answers, agents' event cancellations, booking references retired with no
13
- * booking and feed changes, deleted, and dashboard assistant conversations past their expiry.
14
- * A cancelled booking's tombstone stays.
11
+ * Rows deleted: expired holds and their keyed answers; past retention, event idempotency answers,
12
+ * agent event cancellations, stray booking retirements and feed changes; and expired assistant
13
+ * conversations. A cancelled booking's tombstone stays.
15
14
  */
16
15
  pruned: number;
17
16
  /**
@@ -25,24 +24,31 @@ export interface SlotlockMaintenanceResult {
25
24
  noShows: number;
26
25
  /** Waitlist entries whose window came free, each now holding it (`waitlist.offered`). */
27
26
  waitlistOffers: number;
27
+ /** Approvals no one decided in time, their held time freed (`approval.expired`). */
28
+ approvalsExpired: number;
28
29
  /** More was left than one pass does: run another. */
29
30
  capped: boolean;
30
31
  /**
31
- * The roll stopped before the series it had put back: series it has not tried since may still
32
- * extend, so another pass should follow at once.
32
+ * The roll stopped before the series it had deferred: others may still extend, so run another
33
+ * pass at once.
33
34
  */
34
35
  rollPending: boolean;
35
36
  }
36
37
  /**
37
- * One maintenance pass for a tenant: roll its recurring events to the rolling horizon, then purge
38
- * its expired holds with their answers and prune its events' and bookings' bookkeeping past the
39
- * retention window, each in at most `batches` batches (1-100, default 10), every batch its own
40
- * transaction. Aborting `signal` stops it between batches with the signal's reason.
38
+ * One maintenance pass for a tenant: roll its recurring events to the horizon, then purge expired
39
+ * holds and prune bookkeeping past retention, each in at most `batches` batches (1-100, default
40
+ * 10), each its own transaction. Aborting `signal` stops it between batches.
41
41
  */
42
42
  export declare function maintainSlotlockTenant(store: SlotlockStore, tenantRef: string, options?: {
43
43
  batches?: number;
44
44
  signal?: AbortSignal;
45
45
  followZoneRules?: boolean;
46
+ /**
47
+ * Roll the tenant's committed bookings and holds up into its usage first (usage.ts), before
48
+ * pruning can delete feed changes it has not counted. For a deployment that meters
49
+ * (SLOTLOCK_USAGE=on).
50
+ */
51
+ usage?: boolean;
46
52
  }): Promise<SlotlockMaintenanceResult>;
47
53
  /**
48
54
  * Run `work` holding the tenant's maintenance lock on a connection of its own, or return
@@ -63,12 +69,20 @@ export interface SlotlockMaintenanceSchedulerOptions {
63
69
  sql: Sql;
64
70
  /** Tenants maintained from the start, whether or not a key of theirs calls. */
65
71
  tenants: readonly string[];
72
+ /**
73
+ * Every tenant with data (slotlock.maintenance_tenants), read at the start and once an interval;
74
+ * each not maintained within the interval is queued, so a tenant whose keys never call is
75
+ * maintained too. What it throws goes to onError.
76
+ */
77
+ discover?: () => Promise<readonly string[]>;
66
78
  /** How often each tenant is maintained, at most a day. */
67
79
  intervalMs: number;
68
80
  /** Tenants remembered; the one served longest ago is forgotten first. Default 10,000. */
69
81
  maxTenants?: number;
70
82
  /** Batches of each task per pass (maintainSlotlockTenant). */
71
83
  batches?: number;
84
+ /** Roll each tenant's bookings and holds up into its usage, first (maintainSlotlockTenant `usage`). */
85
+ usage?: boolean;
72
86
  /** A pass that ran: counts only, never the tenant. Not awaited; what it throws is ignored. */
73
87
  onPass?: (result: SlotlockMaintenanceResult) => void;
74
88
  /**
package/maintenance.js CHANGED
@@ -1,26 +1,31 @@
1
1
  import { ignoreRejection } from './hooks.js';
2
2
  import { SLOTLOCK_MAX_WAITLIST_PER_TENANT, calendarEventRollingHorizon, } from './store.js';
3
3
  /**
4
- * One maintenance pass for a tenant: roll its recurring events to the rolling horizon, then purge
5
- * its expired holds with their answers and prune its events' and bookings' bookkeeping past the
6
- * retention window, each in at most `batches` batches (1-100, default 10), every batch its own
7
- * transaction. Aborting `signal` stops it between batches with the signal's reason.
4
+ * One maintenance pass for a tenant: roll its recurring events to the horizon, then purge expired
5
+ * holds and prune bookkeeping past retention, each in at most `batches` batches (1-100, default
6
+ * 10), each its own transaction. Aborting `signal` stops it between batches.
8
7
  */
9
8
  export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
10
9
  const batches = options.batches ?? 10;
11
10
  if (!(Number.isSafeInteger(batches) && batches >= 1 && batches <= 100)) {
12
11
  throw new RangeError('batches must be a whole number from 1 to 100');
13
12
  }
14
- // What a pass changes (a no-show released, a waitlist offer) is recorded as maintenance's.
15
13
  const withTenant = (callback) => store.withTenant(tenantRef, callback, { origin: 'maintenance' });
14
+ if (options.usage) {
15
+ for (let batch = 0; batch < batches; batch++) {
16
+ options.signal?.throwIfAborted();
17
+ const read = await withTenant((tenant) => tenant.rollUpUsage({ tenantRef, limit: USAGE_ROLL_UP_BATCH }));
18
+ if (read < USAGE_ROLL_UP_BATCH)
19
+ break;
20
+ }
21
+ }
16
22
  const window = calendarEventRollingHorizon();
17
23
  let extended = 0;
18
24
  let conflicts = 0;
19
25
  let refused = 0;
20
26
  let rollCapped = true;
21
27
  let after = null;
22
- // A roll that fails does not stop the pruning: expired holds are purged all the same, and the
23
- // roll's error is thrown after.
28
+ // A failed roll does not stop the pruning; its error is thrown after.
24
29
  let rollFailure = null;
25
30
  try {
26
31
  for (let batch = 0; batch < batches; batch++) {
@@ -65,15 +70,16 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
65
70
  }
66
71
  }
67
72
  options.signal?.throwIfAborted();
68
- // Conversations last 2 hours after their last message (dashboard.ts): an expired one is
69
- // unreadable already, and deleted here.
73
+ // Conversations expire 2 hours after their last message (dashboard.ts).
70
74
  pruned += await withTenant((tenant) => tenant.pruneAssistantConversations({ tenantRef }));
71
75
  const zones = await withTenant((tenant) => tenant.followZoneRules({ tenantRef, move: options.followZoneRules === true }));
72
76
  const staleSources = await withTenant((tenant) => tenant.reportStaleCoverage({ tenantRef }));
73
- // No-shows first: the time they free is offered to the waitlist in the same pass.
77
+ // Approvals past their wait, and no-shows, first: the time they free is offered to the waitlist
78
+ // in the same pass.
79
+ const approvals = await withTenant((tenant) => tenant.expireApprovals({ tenantRef, limit: 100 }));
74
80
  const noShows = await withTenant((tenant) => tenant.releaseNoShows({ tenantRef }));
75
- // The whole waitlist, in join order, a page at a time and each page its own transaction, so a
76
- // later entry whose window is free is offered however many wait before it.
81
+ // The whole waitlist in join order, a page per transaction, so a later free entry is offered
82
+ // however many wait before it.
77
83
  let waitlistOffers = 0;
78
84
  let page = null;
79
85
  for (let pages = 0; pages < WAITLIST_PAGES; pages++) {
@@ -96,7 +102,8 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
96
102
  staleSources,
97
103
  noShows,
98
104
  waitlistOffers,
99
- capped: rollCapped || eventsLeft || bookingsLeft || changesLeft,
105
+ approvalsExpired: approvals.expired,
106
+ capped: rollCapped || eventsLeft || bookingsLeft || changesLeft || approvals.expired === 100,
100
107
  rollPending: rollCapped && after !== null && (after.rollRetryAt ?? null) === null,
101
108
  };
102
109
  }
@@ -106,6 +113,8 @@ const WAITLIST_PAGES = Math.ceil(SLOTLOCK_MAX_WAITLIST_PER_TENANT / 50);
106
113
  const SWEEP_INTERVAL_MS = 15 * 60_000;
107
114
  /** Feed changes past the retention deleted per batch. */
108
115
  const CHANGE_PRUNE_BATCH = 1_000;
116
+ /** Feed changes rolled up into usage per batch. */
117
+ const USAGE_ROLL_UP_BATCH = 5_000;
109
118
  /** The advisory lock key for one tenant's maintenance (64-bit, from the tenant reference). */
110
119
  const LOCK_NAMESPACE = 'slotlock.maintenance:';
111
120
  /**
@@ -173,6 +182,7 @@ export function startSlotlockMaintenance(options) {
173
182
  try {
174
183
  const result = await withSlotlockMaintenanceLock(options.sql, tenantRef, () => maintainSlotlockTenant(options.store, tenantRef, {
175
184
  ...(options.batches !== undefined ? { batches: options.batches } : {}),
185
+ ...(options.usage ? { usage: true } : {}),
176
186
  signal: controller.signal,
177
187
  }));
178
188
  recordPass(tenantRef, Date.now());
@@ -183,12 +193,10 @@ export function startSlotlockMaintenance(options) {
183
193
  catch {
184
194
  // Reporting must never stop maintenance, or make a pass that ran look failed.
185
195
  }
186
- // A capped pass that got somewhere goes round again at once, as does one whose roll
187
- // stopped before the series it had put back: each series it could not extend is now
188
- // behind every one it has not tried (the store puts it there), so the next pass meets
189
- // new ones, and however many are stuck, each is met once before a healthy one is
190
- // reached. A pass that reached the series put back waits for the next interval: they
191
- // take turns there, and the next pass reaches anything new before them.
196
+ // A capped pass that progressed, or whose roll stopped before its deferred series, goes
197
+ // again at once: deferred series sit behind untried ones, so each stuck one is met once
198
+ // before a healthy one is reached. A pass that reached the deferred series waits for
199
+ // the next interval.
192
200
  const progressed = result.extended + result.pruned > 0;
193
201
  again = result.capped && (progressed || result.rollPending);
194
202
  }
@@ -215,20 +223,42 @@ export function startSlotlockMaintenance(options) {
215
223
  working = null;
216
224
  }
217
225
  };
218
- // Expired assistant conversations of every tenant, the ones whose keys never call included (a
219
- // tenant only the dashboard serves): one bounded sweep at most every quarter hour.
226
+ const report = (error) => {
227
+ try {
228
+ ignoreRejection(options.onError?.(error));
229
+ }
230
+ catch {
231
+ // Reporting must never stop maintenance.
232
+ }
233
+ };
234
+ // Expired conversations of every tenant, including those no key calls (dashboard-only): one
235
+ // bounded sweep at most every quarter hour.
220
236
  let lastSweep = 0;
221
237
  const sweep = () => {
222
238
  if (controller.signal.aborted || Date.now() - lastSweep < Math.min(intervalMs, SWEEP_INTERVAL_MS))
223
239
  return;
224
240
  lastSweep = Date.now();
225
- options.store.sweepAssistantConversations().catch((error) => {
226
- try {
227
- ignoreRejection(options.onError?.(error));
228
- }
229
- catch {
230
- // Reporting must never stop maintenance.
231
- }
241
+ options.store.sweepAssistantConversations().catch(report);
242
+ };
243
+ // Every tenant with data, once an interval: those no key has called since the server started too.
244
+ let lastDiscovery = Number.NEGATIVE_INFINITY;
245
+ let discovering = false;
246
+ const discover = () => {
247
+ const list = options.discover;
248
+ if (!list || discovering || controller.signal.aborted)
249
+ return;
250
+ if (Date.now() - lastDiscovery < intervalMs)
251
+ return;
252
+ lastDiscovery = Date.now();
253
+ discovering = true;
254
+ (async () => list())()
255
+ .then((tenants) => {
256
+ for (const tenantRef of tenants)
257
+ if (due(tenantRef))
258
+ enqueue(tenantRef);
259
+ }, report)
260
+ .finally(() => {
261
+ discovering = false;
232
262
  });
233
263
  };
234
264
  const tick = () => {
@@ -236,6 +266,7 @@ export function startSlotlockMaintenance(options) {
236
266
  if (due(tenantRef))
237
267
  enqueue(tenantRef);
238
268
  }
269
+ discover();
239
270
  sweep();
240
271
  };
241
272
  const timer = setInterval(tick, Math.min(intervalMs, 60_000));
package/mcp-app.d.ts CHANGED
@@ -5,16 +5,14 @@ export declare const SLOTLOCK_MCP_APP_RESOURCE_URI = "ui://slotlock/calendar";
5
5
  /** True only when the host declares the stable extension's required HTML MIME capability. */
6
6
  export declare function clientSupportsSlotlockMcpApp(capabilities: unknown): boolean;
7
7
  /**
8
- * A static, network-dark calendar view. Dynamic calendar data arrives only through the host's
9
- * `ui/notifications/tool-result` channel; the template contains no tenant or event data and uses
10
- * textContent for every untrusted value. Keeping this as reviewed source instead of generated UI
11
- * also lets MCP hosts inspect and cache the exact resource before rendering it.
8
+ * A static, network-dark calendar view. Data arrives only through the host's
9
+ * `ui/notifications/tool-result` channel; the template holds no tenant data and renders untrusted
10
+ * values with textContent. Reviewed source rather than generated UI, so hosts can inspect and cache
11
+ * it.
12
12
  *
13
- * A certain slot (from `slotlock_find_available_slots` or `slotlock_find_next_available`) can be
14
- * picked: the view sends the host one `ui/message` naming it, in a fixed sentence holding only the
15
- * resource id and the two instants it checked, so the agent books it through the normal path,
16
- * where Slotlock asks a person to confirm the write if it is set to. The view itself never calls a
17
- * tool, and every write tool is visible to the model only.
13
+ * A certain slot can be picked: the view sends one `ui/message` naming only the resource id and its
14
+ * two instants, so the agent books it through the normal path (with confirmation if configured).
15
+ * The view never calls a tool.
18
16
  */
19
17
  export declare const SLOTLOCK_MCP_APP_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <title>Slotlock calendar</title>\n <style>\n :root{color-scheme:light dark;--fb-bg:#fbfbf9;--fb-surface:#ffffff;--fb-sunken:#f3f4f0;--fb-text:#171915;--fb-muted:#5d655b;--fb-faint:#8a9187;--fb-line:#e3e5df;--fb-line-strong:#c9cdc4;--fb-inverse:#171915;--fb-on-inverse:#ffffff;--fb-ring:#2e5bff;--fb-ok:#23855b;--fb-warn:#b7791f;--fb-danger:#9b2c2c}\n @media (prefers-color-scheme:dark){:root:not([data-theme=\"light\"]){--fb-bg:#121411;--fb-surface:#1a1d19;--fb-sunken:#22261f;--fb-text:#eef0ea;--fb-muted:#a9b0a5;--fb-faint:#7d847a;--fb-line:#2c302a;--fb-line-strong:#3d4339;--fb-inverse:#eef0ea;--fb-on-inverse:#121411;--fb-ring:#7d9bff;--fb-ok:#5cc495;--fb-warn:#e0a44a;--fb-danger:#f08a8a}}\n :root[data-theme=\"dark\"]{--fb-bg:#121411;--fb-surface:#1a1d19;--fb-sunken:#22261f;--fb-text:#eef0ea;--fb-muted:#a9b0a5;--fb-faint:#7d847a;--fb-line:#2c302a;--fb-line-strong:#3d4339;--fb-inverse:#eef0ea;--fb-on-inverse:#121411;--fb-ring:#7d9bff;--fb-ok:#5cc495;--fb-warn:#e0a44a;--fb-danger:#f08a8a}\n :root{--bg:var(--color-background-primary,var(--fb-bg));--surface:var(--color-background-secondary,var(--fb-surface));--sunken:var(--color-background-tertiary,var(--fb-sunken));--text:var(--color-text-primary,var(--fb-text));--muted:var(--color-text-secondary,var(--fb-muted));--faint:var(--color-text-tertiary,var(--fb-faint));--line:var(--color-border-primary,var(--fb-line));--line-strong:var(--color-border-secondary,var(--fb-line-strong));--inverse:var(--color-background-inverse,var(--fb-inverse));--on-inverse:var(--color-text-inverse,var(--fb-on-inverse));--ring:var(--color-ring-primary,var(--fb-ring));--ok:var(--color-text-success,var(--fb-ok));--danger:var(--color-text-danger,var(--fb-danger));font-family:var(--font-sans,ui-sans-serif,system-ui,-apple-system,\"Segoe UI\",sans-serif);color:var(--text);background:var(--bg)}\n *{box-sizing:border-box}body{margin:0;min-width:0;background:transparent;-webkit-font-smoothing:antialiased}button,input,textarea{font:inherit;color:inherit}p{margin:0}\n button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--ring);outline-offset:2px}\n .shell{display:grid;gap:12px;padding:16px;max-width:760px;margin:0 auto}\n .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}\n .status{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}.dot{width:7px;height:7px;border-radius:50%;background:var(--fb-warn)}.status[data-ready=\"true\"] .dot{background:var(--ok)}\n /* The picker */\n .picker{display:grid;border:1px solid var(--line);border-radius:16px;background:var(--surface);overflow:hidden}.picker[hidden]{display:none}\n .head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:16px 16px 12px}\n .head h1{margin:0;font-size:var(--font-heading-sm-size,18px);font-weight:var(--font-weight-semibold,600);letter-spacing:-.01em}\n .meta{margin-top:4px;font-size:13px;color:var(--muted);display:flex;flex-wrap:wrap;gap:4px 10px}.meta span+span::before{content:\"\u00B7\";margin-right:10px;color:var(--faint)}\n .clock{display:inline-flex;padding:2px;border-radius:8px;background:var(--sunken);flex:none}.clock button{min-width:40px;padding:4px 8px;border:0;border-radius:6px;background:transparent;font-size:12px;color:var(--muted);cursor:pointer}.clock button[aria-pressed=\"true\"]{background:var(--surface);color:var(--text);box-shadow:0 1px 2px rgba(0,0,0,.08)}\n .days{display:flex;gap:6px;padding:0 16px 12px;overflow-x:auto;scrollbar-width:none;scroll-snap-type:x proximity}.days::-webkit-scrollbar{display:none}.days[hidden]{display:none}\n .day{flex:none;display:grid;justify-items:center;gap:1px;width:56px;padding:8px 0 7px;border:1px solid var(--line);border-radius:12px;background:transparent;cursor:pointer;scroll-snap-align:start;font-variant-numeric:tabular-nums}\n .day small{font-size:11px;color:var(--muted)}.day strong{font-size:17px;font-weight:var(--font-weight-semibold,600);line-height:1.2}.day i{font-style:normal;font-size:10px;color:var(--faint)}\n .day:hover{border-color:var(--line-strong)}.day[aria-pressed=\"true\"]{background:var(--inverse);border-color:var(--inverse);color:var(--on-inverse)}.day[aria-pressed=\"true\"] small,.day[aria-pressed=\"true\"] i{color:inherit;opacity:.75}\n .times{display:grid;gap:14px;padding:14px 16px 16px;border-top:1px solid var(--line)}.times[hidden],.choice[hidden]{display:none}.foot:has(.choice[hidden]){justify-content:flex-end}\n .part h2{margin:0 0 8px;font-size:11px;font-weight:var(--font-weight-medium,500);letter-spacing:.06em;text-transform:uppercase;color:var(--faint)}\n .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(96px,1fr));gap:8px}\n .time{display:grid;justify-items:center;gap:1px;min-height:42px;padding:8px 6px;border:1px solid var(--line-strong);border-radius:10px;background:var(--surface);cursor:pointer;font-variant-numeric:tabular-nums;font-weight:var(--font-weight-medium,500);transition:border-color .12s,background-color .12s}\n .time small{font-size:10px;font-weight:400;color:var(--muted)}.time:hover:not(:disabled){border-color:var(--text)}\n .time[aria-pressed=\"true\"]{background:var(--inverse);border-color:var(--inverse);color:var(--on-inverse)}.time[aria-pressed=\"true\"] small{color:inherit;opacity:.75}.time:disabled{cursor:default}.time:disabled:not([aria-pressed=\"true\"]){opacity:.45}\n .one{display:grid;gap:4px;padding:4px 16px 16px}.one strong{font-size:var(--font-heading-md-size,22px);font-weight:var(--font-weight-semibold,600);letter-spacing:-.01em;font-variant-numeric:tabular-nums}.one span{font-size:14px;color:var(--muted)}.one[hidden]{display:none}\n .foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-top:1px solid var(--line);background:var(--sunken)}\n .choice{min-width:0;font-size:13px;color:var(--muted);overflow-wrap:anywhere}.choice b{display:block;color:var(--text);font-weight:var(--font-weight-semibold,600)}\n .book{flex:none;min-height:40px;padding:9px 16px;border:0;border-radius:10px;background:var(--inverse);color:var(--on-inverse);font-weight:var(--font-weight-semibold,600);cursor:pointer}.book:disabled{cursor:default;opacity:.4}.book[data-sent=\"true\"]{opacity:1;background:transparent;color:var(--ok);padding-inline:4px}\n .fallback{display:grid;gap:6px;padding:12px 16px;border-top:1px solid var(--line);font-size:13px;color:var(--muted)}.fallback[hidden]{display:none}.fallback textarea{width:100%;min-height:64px;padding:8px;border:1px solid var(--line-strong);border-radius:8px;background:var(--bg);font:12px/1.5 var(--font-mono,ui-monospace,monospace);resize:vertical}\n /* Every other result */\n .list[hidden]{display:none}.list{display:grid;gap:12px}\n .mast{display:flex;align-items:center;justify-content:space-between;gap:12px}.mast h1{margin:0;font-size:var(--font-heading-sm-size,18px);font-weight:var(--font-weight-semibold,600)}\n .toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.search{flex:1;min-width:min(100%,220px);min-height:38px;padding:8px 12px;border:1px solid var(--line-strong);border-radius:10px;background:var(--surface)}.summary{font-size:13px;color:var(--muted)}\n .panel{min-width:0;border:1px solid var(--line);border-radius:14px;background:var(--surface);overflow:hidden}.items{display:grid;min-width:0}\n .item{display:grid;grid-template-columns:minmax(120px,.85fr) minmax(0,1.5fr) auto;gap:12px;align-items:start;padding:12px 14px;border-bottom:1px solid var(--line)}.item:last-child{border-bottom:0}.item-title{font-weight:var(--font-weight-semibold,600);overflow-wrap:anywhere}.item-detail{min-width:0;color:var(--muted);font-size:13px;line-height:1.45;overflow-wrap:anywhere}.badge{justify-self:end;padding:3px 7px;border:1px solid var(--line);border-radius:6px;font-size:11px;color:var(--muted);white-space:nowrap}.empty{padding:28px 16px;text-align:center;color:var(--muted);font-size:14px}\n details{border-top:1px solid var(--line)}summary{cursor:pointer;padding:10px 14px;color:var(--muted);font-size:12px}pre{margin:0;padding:14px;max-height:260px;overflow:auto;font:12px/1.5 var(--font-mono,ui-monospace,monospace);white-space:pre-wrap;overflow-wrap:anywhere;background:var(--sunken)}\n @media(max-width:520px){.shell{padding:10px}.item{grid-template-columns:1fr}.badge{justify-self:start}.grid{grid-template-columns:repeat(auto-fill,minmax(84px,1fr))}.foot{flex-wrap:wrap}.book{flex:1}}\n @media(prefers-reduced-motion:reduce){.time{transition:none}}\n </style>\n</head>\n<body>\n <main class=\"shell\">\n <section class=\"picker\" id=\"picker\" aria-labelledby=\"pickTitle\" hidden>\n <div class=\"head\">\n <div><h1 id=\"pickTitle\">Pick a time</h1><p class=\"meta\" id=\"pickMeta\"></p></div>\n <div class=\"clock\" role=\"group\" aria-label=\"Clock\"><button type=\"button\" id=\"h12\" aria-pressed=\"false\">12h</button><button type=\"button\" id=\"h24\" aria-pressed=\"false\">24h</button></div>\n </div>\n <div class=\"days\" id=\"days\" role=\"group\" aria-label=\"Day\"></div>\n <div class=\"one\" id=\"one\" hidden></div>\n <div class=\"times\" id=\"times\"></div>\n <div class=\"foot\">\n <p class=\"choice\" id=\"choice\" aria-live=\"polite\">No time picked</p>\n <button type=\"button\" class=\"book\" id=\"book\" disabled>Book this time</button>\n </div>\n <div class=\"fallback\" id=\"fallback\" hidden><p>Your host did not pass the choice on. Send this to your agent:</p><textarea id=\"fallbackText\" readonly></textarea></div>\n </section>\n <section class=\"list\" id=\"list\">\n <header class=\"mast\"><h1>Slotlock calendar</h1><p class=\"status\" id=\"status\" data-ready=\"false\" role=\"status\"><span class=\"dot\" aria-hidden=\"true\"></span><span id=\"statusText\">Connecting to host</span></p></header>\n <div class=\"toolbar\">\n <label class=\"sr\" for=\"filter\">Filter this result</label><input id=\"filter\" class=\"search\" type=\"search\" autocomplete=\"off\" placeholder=\"Filter by resource, event or time\">\n <p class=\"summary\" id=\"summary\" aria-live=\"polite\">Waiting for a calendar result.</p>\n </div>\n <div class=\"panel\">\n <div class=\"items\" id=\"items\"><p class=\"empty\">Ask your agent to list resources, inspect free-busy, find a slot, or list events.</p></div>\n <details><summary>Structured result</summary><pre id=\"raw\">No result yet.</pre></details>\n </div>\n </section>\n </main>\n <script>\n (() => {\n 'use strict';\n const state = { data: null, query: '', requestId: 1, nextId: 2, timeZone: undefined, locale: undefined, hour12: undefined, day: null, chosen: null, sent: false, pending: {}, size: '' };\n const $ = (id) => document.getElementById(id);\n const status = $('status'), statusText = $('statusText'), items = $('items'), summary = $('summary'), raw = $('raw'), filter = $('filter');\n const picker = $('picker'), list = $('list'), days = $('days'), times = $('times'), one = $('one'), choice = $('choice'), book = $('book'), pickMeta = $('pickMeta'), fallback = $('fallback'), fallbackText = $('fallbackText'), h12 = $('h12'), h24 = $('h24');\n const safeText = (value) => value === null || value === undefined ? '\u2014' : String(value);\n const post = (message) => window.parent.postMessage(message, '*');\n const setStatus = (text, ready) => { statusText.textContent = text; status.dataset.ready = ready ? 'true' : 'false'; };\n const clear = (node) => { while (node.firstChild) node.removeChild(node.firstChild); };\n const el = (tag, className, text) => { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; };\n\n // The host's look: its standard theme variables (only these names, as plain values) and its theme.\n const THEME_KEYS = /^--(color-(background|text|border|ring)-[a-z]+|font-(sans|mono)|font-weight-[a-z]+|font-(text|heading)-[a-z]+-size)$/;\n const applyHost = (context) => {\n if (!context || typeof context !== 'object') return;\n if (context.theme === 'light' || context.theme === 'dark') document.documentElement.dataset.theme = context.theme;\n const variables = context.styles && context.styles.variables;\n if (variables && typeof variables === 'object') {\n Object.entries(variables).forEach(([key, value]) => {\n if (THEME_KEYS.test(key) && typeof value === 'string' && value.length <= 200 && !/url\\(|expression|[;{}<>]/i.test(value)) document.documentElement.style.setProperty(key, value);\n });\n }\n if (typeof context.timeZone === 'string') state.timeZone = context.timeZone;\n if (typeof context.locale === 'string') state.locale = context.locale;\n };\n\n // Only what the view has checked goes into the message a person sends: an id of plain\n // characters and two instants it parsed itself, never a title or any other server text.\n const SAFE_ID = /^[A-Za-z0-9._:-]{1,200}$/;\n const instant = (value) => { const time = typeof value === 'string' ? Date.parse(value) : NaN; return Number.isFinite(time) ? new Date(time) : null; };\n const HOLD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n const pickable = (slot) => {\n if (!slot || typeof slot.resource_id !== 'string' || !SAFE_ID.test(slot.resource_id)) return null;\n const start = instant(slot.start); const end = instant(slot.end);\n if (!start || !end || end <= start) return null;\n if (slot.hold_id === undefined) return { resource_id: slot.resource_id, start, end };\n // A held time (slotlock_hold_slots): booking it names the hold, so it needs a plain id.\n const expires = instant(slot.expires_at);\n return typeof slot.hold_id === 'string' && HOLD_ID.test(slot.hold_id) && expires\n ? { resource_id: slot.resource_id, start, end, hold_id: slot.hold_id, expires }\n : null;\n };\n const slotsOf = (data) => {\n if (!data || typeof data !== 'object' || data.error) return [];\n // Held time is free for this agent alone until it expires: certain by being held.\n if (Array.isArray(data.holds)) return data.holds.map(pickable).filter((slot) => slot && slot.hold_id);\n if (data.coverage?.certainty !== 'certain') return [];\n const listed = Array.isArray(data.slots) ? data.slots : 'start' in data ? [data] : [];\n return listed.map(pickable).filter(Boolean);\n };\n\n const fmt = (date, options) => {\n const zone = state.timeZone ? { timeZone: state.timeZone } : {};\n try { return new Intl.DateTimeFormat(state.locale, { ...options, ...zone }).format(date); }\n catch { return new Intl.DateTimeFormat(undefined, options).format(date); }\n };\n const prefers12 = () => { try { const cycle = new Intl.DateTimeFormat(state.locale, { hour: 'numeric' }).resolvedOptions().hourCycle; return cycle === 'h11' || cycle === 'h12'; } catch { return false; } };\n const hour12 = () => state.hour12 === undefined ? prefers12() : state.hour12;\n const dayKey = (date) => fmt(date, { year: 'numeric', month: '2-digit', day: '2-digit' });\n const dayLabel = (date) => fmt(date, { weekday: 'short', day: 'numeric', month: 'short' });\n const timeLabel = (date) => fmt(date, { hour: hour12() ? 'numeric' : '2-digit', minute: '2-digit', hour12: hour12() });\n // The hour as a number in any locale: German formats it as \"10 Uhr\", so read the hour part.\n const hourOf = (date) => {\n const zone = state.timeZone ? { timeZone: state.timeZone } : {};\n let parts;\n try { parts = new Intl.DateTimeFormat('en-US', { hour: 'numeric', hourCycle: 'h23', ...zone }).formatToParts(date); }\n catch { parts = new Intl.DateTimeFormat('en-US', { hour: 'numeric', hourCycle: 'h23' }).formatToParts(date); }\n return Number(parts.find((part) => part.type === 'hour')?.value) % 24;\n };\n const minutes = (slot) => Math.round((slot.end - slot.start) / 60000);\n const lengthLabel = (total) => total < 60 ? total + ' min' : Math.floor(total / 60) + ' h' + (total % 60 ? ' ' + (total % 60) + ' min' : '');\n const sentence = (slot) => 'Book the slot I picked: ' + (slot.hold_id ? 'hold ' + slot.hold_id + ', ' : '') + 'resource ' + slot.resource_id + ', from ' + slot.start.toISOString() + ' to ' + slot.end.toISOString() + ' (' + dayLabel(slot.start) + ', ' + timeLabel(slot.start) + ' to ' + timeLabel(slot.end) + (state.timeZone ? ' ' + state.timeZone : '') + ').';\n\n const renderPicker = (slots) => {\n const resources = [...new Set(slots.map((slot) => slot.resource_id))];\n const lengths = new Set(slots.map(minutes));\n clear(pickMeta);\n if (lengths.size === 1) pickMeta.append(el('span', '', lengthLabel(minutes(slots[0]))));\n if (resources.length > 1) pickMeta.append(el('span', '', resources.length + ' resources'));\n pickMeta.append(el('span', '', state.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'Local time'));\n const held = slots.filter((slot) => slot.expires).map((slot) => slot.expires).sort((a, b) => a - b)[0];\n if (held) pickMeta.append(el('span', 'held', 'Held until ' + timeLabel(held)));\n h12.setAttribute('aria-pressed', String(hour12())); h24.setAttribute('aria-pressed', String(!hour12()));\n const byDay = new Map();\n slots.forEach((slot) => { const key = dayKey(slot.start); if (!byDay.has(key)) byDay.set(key, []); byDay.get(key).push(slot); });\n if (!byDay.has(state.day)) state.day = byDay.keys().next().value;\n const single = slots.length === 1;\n $('pickTitle').textContent = single ? 'Next available' : 'Pick a time';\n clear(days); clear(times); clear(one);\n days.hidden = single || byDay.size < 2;\n one.hidden = !single;\n times.hidden = single;\n if (single) {\n if (!state.chosen) state.chosen = slots[0];\n one.append(el('strong', '', timeLabel(slots[0].start) + '\u2013' + timeLabel(slots[0].end)), el('span', '', fmt(slots[0].start, { weekday: 'long', day: 'numeric', month: 'long' })));\n } else {\n byDay.forEach((daySlots, key) => {\n const first = daySlots[0].start;\n const button = el('button', 'day');\n button.type = 'button';\n button.setAttribute('aria-pressed', String(key === state.day));\n button.setAttribute('aria-label', dayLabel(first) + ', ' + daySlots.length + (daySlots.length === 1 ? ' time' : ' times'));\n button.append(el('small', '', fmt(first, { weekday: 'short' })), el('strong', '', fmt(first, { day: 'numeric' })), el('i', '', fmt(first, { month: 'short' })));\n button.addEventListener('click', () => { state.day = key; render(); });\n days.append(button);\n });\n const parts = [['Morning', (h) => h < 12], ['Afternoon', (h) => h >= 12 && h < 17], ['Evening', (h) => h >= 17]];\n const daySlots = byDay.get(state.day) || [];\n parts.forEach(([name, test]) => {\n const inPart = daySlots.filter((slot) => test(hourOf(slot.start)));\n if (inPart.length === 0) return;\n const part = el('section', 'part'); const grid = el('div', 'grid');\n part.append(el('h2', '', name), grid);\n inPart.forEach((slot) => {\n const button = el('button', 'time', timeLabel(slot.start));\n button.type = 'button';\n const isChosen = state.chosen && state.chosen.resource_id === slot.resource_id && +state.chosen.start === +slot.start;\n button.setAttribute('aria-pressed', String(Boolean(isChosen)));\n button.setAttribute('aria-label', dayLabel(slot.start) + ', ' + timeLabel(slot.start) + ' to ' + timeLabel(slot.end) + (resources.length > 1 ? ', resource ' + (resources.indexOf(slot.resource_id) + 1) : ''));\n if (resources.length > 1) button.append(el('small', '', 'Resource ' + (resources.indexOf(slot.resource_id) + 1)));\n else if (lengths.size > 1) button.append(el('small', '', lengthLabel(minutes(slot))));\n button.disabled = state.sent;\n button.addEventListener('click', () => { state.chosen = slot; fallback.hidden = true; render(); });\n grid.append(button);\n });\n times.append(part);\n });\n }\n clear(choice);\n choice.hidden = single;\n if (state.chosen) {\n choice.append(el('b', '', dayLabel(state.chosen.start) + ', ' + timeLabel(state.chosen.start) + '\u2013' + timeLabel(state.chosen.end)));\n if (resources.length > 1) choice.append(document.createTextNode('Resource ' + (resources.indexOf(state.chosen.resource_id) + 1)));\n } else choice.textContent = 'No time picked';\n book.disabled = !state.chosen || state.sent;\n book.dataset.sent = String(state.sent);\n book.textContent = state.sent ? '\u2713 Sent to your agent' : 'Book this time';\n };\n book.addEventListener('click', () => {\n if (!state.chosen || state.sent) return;\n state.sent = true;\n fallback.hidden = true;\n const text = sentence(state.chosen);\n const id = state.nextId++;\n state.pending[id] = text;\n setStatus('Sent to your agent', true);\n post({ jsonrpc: '2.0', id, method: 'ui/message', params: { role: 'user', content: { type: 'text', text } } });\n render();\n });\n h12.addEventListener('click', () => { state.hour12 = true; render(); });\n h24.addEventListener('click', () => { state.hour12 = false; render(); });\n\n const reportSize = () => {\n const root = document.documentElement;\n const size = { width: Math.ceil(root.scrollWidth), height: Math.ceil(root.scrollHeight) };\n const key = size.width + 'x' + size.height;\n if (key === state.size) return;\n state.size = key;\n post({ jsonrpc: '2.0', method: 'ui/notifications/size-changed', params: size });\n };\n const intervalText = (entry) => [entry.start || entry.starts_at, entry.end || entry.ends_at].filter(Boolean).join(' \u2192 ');\n // A refused call carries no structuredContent (hosts validate it against the tool's\n // outputSchema); its text content is {\"error\":{\"code\":\u2026}}.\n const refusalOf = (content) => {\n const text = Array.isArray(content) ? content.find((part) => part && part.type === 'text')?.text : undefined;\n try {\n const parsed = JSON.parse(text);\n if (parsed && parsed.error && typeof parsed.error === 'object') return { error: parsed.error };\n } catch {}\n return { error: { code: 'request_refused' } };\n };\n const flatten = (data) => {\n if (!data || typeof data !== 'object') return [];\n if (data.error) return [{ title: data.error.code || 'Unable to load', detail: 'The calendar server refused this request.', badge: 'Error' }];\n if (Array.isArray(data.resources)) return data.resources.flatMap((resource) => {\n if (Array.isArray(resource.busy)) return resource.busy.map((busy) => ({ title: resource.resource_id, detail: intervalText(busy), badge: resource.coverage?.certainty || 'busy' }));\n return [{ title: resource.external_ref || resource.id || resource.resource_id, detail: resource.timezone || 'Calendar resource', badge: resource.coverage?.certainty || 'resource' }];\n });\n if (Array.isArray(data.slots)) {\n if (data.coverage?.certainty !== 'certain') return [{ title: 'Availability unproven', detail: 'Required calendar coverage is incomplete.', badge: data.coverage?.certainty || 'uncertain' }];\n return data.slots.length === 0 ? [{ title: 'No free time', detail: 'Nothing of that length is free in the window.', badge: 'certain' }] : data.slots.map((slot) => ({ title: slot.resource_id, detail: intervalText(slot), badge: 'free' }));\n }\n if (Array.isArray(data.holds)) return [{ title: 'Nothing held', detail: 'Every time asked for was taken.', badge: 'refused' }];\n if (Array.isArray(data.events)) return data.events.map((event) => ({ title: event.title || event.id, detail: intervalText(event), badge: event.status || event.transparency || 'event' }));\n if (data.event && typeof data.event === 'object') return [{ title: data.event.title || data.event.id, detail: intervalText(data.event), badge: data.event.status || 'event' }];\n if ('start' in data || 'end' in data) {\n const coverage = data.coverage && typeof data.coverage === 'object' ? data.coverage : null;\n const certainty = coverage?.certainty;\n const completeInterval = typeof data.start === 'string' && typeof data.end === 'string';\n if (certainty !== 'certain' || !completeInterval) {\n const reason = typeof coverage?.reason === 'string' ? coverage.reason.replaceAll('_', ' ') : 'Required calendar coverage is incomplete.';\n return [{ title: 'Availability unproven', detail: reason, badge: certainty || 'uncertain' }];\n }\n return [{ title: data.resource_id || 'Next available', detail: intervalText(data), badge: certainty }];\n }\n return Object.entries(data).slice(0, 100).map(([key, value]) => ({ title: key, detail: typeof value === 'object' ? JSON.stringify(value) : safeText(value), badge: 'value' }));\n };\n const line = (title, detail, badge) => {\n const row = el('article', 'item');\n row.append(el('div', 'item-title', safeText(title)), el('div', 'item-detail', safeText(detail)), el('div', 'badge', safeText(badge)));\n return row;\n };\n const render = () => {\n const slots = slotsOf(state.data);\n picker.hidden = slots.length === 0;\n list.hidden = slots.length > 0;\n if (slots.length > 0) { renderPicker(slots); reportSize(); return; }\n clear(items);\n const rows = flatten(state.data).filter((entry) => (entry.title + ' ' + entry.detail + ' ' + entry.badge).toLowerCase().includes(state.query));\n if (rows.length === 0) items.append(el('p', 'empty', state.data ? 'No rows match this filter.' : 'Waiting for a calendar result.'));\n else rows.forEach((entry) => items.append(line(entry.title, entry.detail, entry.badge)));\n summary.textContent = rows.length === 1 ? '1 calendar row' : rows.length + ' calendar rows';\n raw.textContent = state.data ? JSON.stringify(state.data, null, 2) : 'No result yet.';\n reportSize();\n };\n filter.addEventListener('input', () => { state.query = filter.value.trim().toLowerCase(); render(); });\n window.addEventListener('message', (event) => {\n if (event.source !== window.parent) return;\n const message = event.data;\n if (!message || message.jsonrpc !== '2.0') return;\n if (message.id === state.requestId && message.result) {\n applyHost(message.result.hostContext);\n post({ jsonrpc: '2.0', method: 'ui/notifications/initialized' });\n setStatus('Connected', true);\n render();\n return;\n }\n if (message.id in state.pending) {\n const text = state.pending[message.id];\n delete state.pending[message.id];\n if (message.error) {\n // Refused or unsupported: the person can still send it, and pick again.\n state.sent = false;\n fallbackText.value = text;\n fallback.hidden = false;\n setStatus('Not sent', false);\n render();\n }\n return;\n }\n if (message.method === 'ui/notifications/tool-result') {\n const result = message.params || {};\n state.data = result.structuredContent || (result.isError ? refusalOf(result.content) : null);\n state.chosen = null; state.sent = false; state.day = null;\n fallback.hidden = true;\n setStatus(result.isError ? 'Request refused' : 'Result current', !result.isError);\n render();\n }\n if (message.method === 'ui/notifications/host-context-changed') { applyHost(message.params); render(); }\n });\n if (typeof ResizeObserver === 'function') new ResizeObserver(reportSize).observe(document.documentElement);\n post({ jsonrpc: '2.0', id: state.requestId, method: 'ui/initialize', params: { appInfo: { name: 'slotlock-calendar', version: '1.1.0' }, appCapabilities: { availableDisplayModes: ['inline', 'fullscreen'] }, protocolVersion: '2026-01-26' } });\n })();\n </script>\n</body>\n</html>";
20
18
  export declare const SLOTLOCK_MCP_APP_RESOURCE: Readonly<{
package/mcp-app.js CHANGED
@@ -16,16 +16,14 @@ export function clientSupportsSlotlockMcpApp(capabilities) {
16
16
  return Array.isArray(mimeTypes) && mimeTypes.includes(SLOTLOCK_MCP_APP_MIME_TYPE);
17
17
  }
18
18
  /**
19
- * A static, network-dark calendar view. Dynamic calendar data arrives only through the host's
20
- * `ui/notifications/tool-result` channel; the template contains no tenant or event data and uses
21
- * textContent for every untrusted value. Keeping this as reviewed source instead of generated UI
22
- * also lets MCP hosts inspect and cache the exact resource before rendering it.
19
+ * A static, network-dark calendar view. Data arrives only through the host's
20
+ * `ui/notifications/tool-result` channel; the template holds no tenant data and renders untrusted
21
+ * values with textContent. Reviewed source rather than generated UI, so hosts can inspect and cache
22
+ * it.
23
23
  *
24
- * A certain slot (from `slotlock_find_available_slots` or `slotlock_find_next_available`) can be
25
- * picked: the view sends the host one `ui/message` naming it, in a fixed sentence holding only the
26
- * resource id and the two instants it checked, so the agent books it through the normal path,
27
- * where Slotlock asks a person to confirm the write if it is set to. The view itself never calls a
28
- * tool, and every write tool is visible to the model only.
24
+ * A certain slot can be picked: the view sends one `ui/message` naming only the resource id and its
25
+ * two instants, so the agent books it through the normal path (with confirmation if configured).
26
+ * The view never calls a tool.
29
27
  */
30
28
  export const SLOTLOCK_MCP_APP_HTML = `<!doctype html>
31
29
  <html lang="en">
package/mcp-modern.d.ts CHANGED
@@ -16,11 +16,10 @@ export interface ModernMcpRefusal {
16
16
  data?: Record<string, unknown>;
17
17
  }
18
18
  /**
19
- * Validate a 2026-07-28 request's `_meta` envelope and the HTTP headers that mirror its body, in the
20
- * specification's order: the required `_meta` fields (-32602, basic §_meta), `MCP-Protocol-Version`
21
- * against the body (-32020), a revision the server implements (-32022, basic/versioning), then
22
- * `Mcp-Method` and `Mcp-Name` against the body, the latter after Base64-sentinel decoding (-32020,
23
- * streamable-http §Server Validation). Every refusal is HTTP 400.
19
+ * Validate a 2026-07-28 request's `_meta` envelope and the headers mirroring its body, in the
20
+ * specification's order: required `_meta` fields (-32602), `MCP-Protocol-Version` against the body
21
+ * (-32020), a supported revision (-32022), then `Mcp-Method` and `Mcp-Name` against the body
22
+ * (-32020). Every refusal is HTTP 400.
24
23
  */
25
24
  export declare function checkModernMcpRequest(args: {
26
25
  headers: Headers;
@@ -40,13 +39,13 @@ export declare function sha256Hex(value: string): string;
40
39
  /** Validate and copy the HMAC keys that seal `requestState`: 1-8 keys of at least 32 bytes each. */
41
40
  export declare function stateSecrets(secrets: readonly (string | Uint8Array)[]): Buffer[];
42
41
  /**
43
- * Seal a JSON payload for a client to carry and echo: `v1.<payload>.<HMAC-SHA256>`, both base64url.
44
- * It is integrity-protected, not encrypted, so it must never hold anything the client may not see.
42
+ * Seal a JSON payload for a client to echo: `v1.<payload>.<HMAC-SHA256>`, base64url.
43
+ * Integrity-protected, not encrypted: never put anything the client may not see in it.
45
44
  */
46
45
  export declare function sealRequestState(key: Buffer, payload: Record<string, unknown>): string;
47
46
  /**
48
- * The payload of a state this server sealed with any of `keys`, and the key that verified it; `null`
49
- * for anything else. The MAC is checked in constant time before the payload is parsed.
47
+ * The payload of a state this server sealed with any of `keys`, and the verifying key; `null`
48
+ * otherwise. The MAC is checked in constant time before parsing.
50
49
  */
51
50
  export declare function openRequestState(keys: readonly Buffer[], state: unknown): {
52
51
  payload: Record<string, unknown>;
@@ -69,9 +68,9 @@ export interface McpEventStream {
69
68
  close(): void;
70
69
  }
71
70
  /**
72
- * An SSE response (streamable-http §Receiving Messages): never cached, not buffered by a reverse
73
- * proxy (`X-Accel-Buffering: no`). `onCancel` runs once when the reader goes away first — the client
74
- * closed the stream, which on HTTP is how it cancels the request.
71
+ * An SSE response (streamable-http §Receiving Messages): uncached and unbuffered by proxies
72
+ * (`X-Accel-Buffering: no`). `onCancel` runs once if the reader leaves first, which is how an HTTP
73
+ * client cancels.
75
74
  */
76
75
  export declare function openMcpEventStream(onCancel: () => void): McpEventStream;
77
76
  /** W3C Trace Context (and Baggage) a request carried, validated; handed to the backend as is. */
@@ -81,9 +80,8 @@ export interface SlotlockTraceContext {
81
80
  baggage?: string;
82
81
  }
83
82
  /**
84
- * Trace context from the request's `_meta` (MCP reserves `traceparent`, `tracestate` and `baggage`
85
- * there) or, when `_meta` has none, from the HTTP headers of the same names. One source only, never
86
- * a mix; an invalid `traceparent` discards the context, and an invalid `tracestate` or `baggage` is
87
- * dropped on its own (W3C Trace Context §3.3, Baggage §3.3).
83
+ * Trace context from `_meta` (MCP reserves `traceparent`, `tracestate` and `baggage` there) or, if
84
+ * absent, the HTTP headers; never a mix. An invalid `traceparent` discards it all; an invalid
85
+ * `tracestate` or `baggage` is dropped alone (W3C Trace Context §3.3, Baggage §3.3).
88
86
  */
89
87
  export declare function traceContextFrom(meta: Record<string, unknown> | undefined, headers: Headers): SlotlockTraceContext | undefined;