slotlock 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +237 -0
  2. package/agent-server.d.ts +25 -5
  3. package/agent-server.js +152 -7
  4. package/agent-store-backend.d.ts +6 -1
  5. package/agent-store-backend.js +861 -55
  6. package/application-api.d.ts +19 -1
  7. package/application-api.js +578 -21
  8. package/approvals.d.ts +2 -0
  9. package/approvals.js +12 -5
  10. package/assistant.d.ts +34 -8
  11. package/assistant.js +81 -26
  12. package/availability-store.d.ts +193 -0
  13. package/availability-store.js +558 -0
  14. package/booking-rules.d.ts +92 -5
  15. package/booking-rules.js +243 -15
  16. package/dashboard-assistant-ui.d.ts +1 -1
  17. package/dashboard-assistant-ui.js +109 -8
  18. package/dashboard.d.ts +55 -6
  19. package/dashboard.js +790 -105
  20. package/ddl.d.ts +5 -1
  21. package/ddl.js +760 -9
  22. package/event-feed.d.ts +1 -1
  23. package/event-feed.js +18 -0
  24. package/holiday-import.d.ts +38 -0
  25. package/holiday-import.js +57 -0
  26. package/holidays.d.ts +38 -0
  27. package/holidays.js +150 -0
  28. package/hours.d.ts +125 -0
  29. package/hours.js +224 -0
  30. package/index.d.ts +14 -3
  31. package/index.js +10 -2
  32. package/interop.d.ts +25 -7
  33. package/interop.js +145 -17
  34. package/lifecycle.d.ts +31 -0
  35. package/lifecycle.js +115 -0
  36. package/maintenance.d.ts +19 -0
  37. package/maintenance.js +55 -1
  38. package/metrics.js +3 -0
  39. package/openapi.json +16237 -7795
  40. package/package.json +1 -1
  41. package/ranges.d.ts +28 -0
  42. package/ranges.js +135 -0
  43. package/resource-search.d.ts +19 -0
  44. package/resource-search.js +153 -0
  45. package/rest-api.js +7 -0
  46. package/rules.d.ts +2 -0
  47. package/rules.js +2 -0
  48. package/self-host.d.ts +1 -1
  49. package/self-host.js +221 -21
  50. package/slotlock.manifest.json +1 -1
  51. package/store.d.ts +367 -24
  52. package/store.js +1567 -97
  53. package/types.d.ts +198 -0
package/self-host.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // supplies it the same way and nothing is read from a file or a request. A missing or weak secret
4
4
  // stops a command before it connects anywhere, and no secret is written to a log line or an error.
5
5
  // The one exception is the API key `key create` prints: that output is the key's only copy.
6
- import { createSlotlockWebhookSender, deliverSlotlockWebhooks, startSlotlockWebhookDelivery, } from './webhooks.js';
6
+ import { createSlotlockGuardedFetch, createSlotlockWebhookSender, deliverSlotlockWebhooks, startSlotlockWebhookDelivery, } from './webhooks.js';
7
7
  import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
8
8
  import { parseArgs } from 'node:util';
9
9
  import postgres from 'postgres';
@@ -21,6 +21,7 @@ import { createSlotlockDashboardTeams, createSlotlockOrganizationCheck } from '.
21
21
  import { SLOTLOCK_DEFAULT_OVERAGE_STOP_PERCENT, reportSlotlockOverage, slotlockMonthOf } from './overage.js';
22
22
  import { createSlotlockOtlpExporter, createSlotlockOtlpMetricsExporter, readSlotlockOtlpConfig, } from './otel.js';
23
23
  import { readFile } from 'node:fs/promises';
24
+ import { readSlotlockHolidayCalendar } from './holiday-import.js';
24
25
  import { SLOTLOCK_DASHBOARD_FUNCTIONS, SLOTLOCK_QUOTA_FUNCTIONS, SLOTLOCK_TENANT_TABLES, SLOTLOCK_USAGE_FUNCTIONS, } from './ddl.js';
25
26
  import { maintainSlotlockTenant, startSlotlockMaintenance, withSlotlockMaintenanceLock, } from './maintenance.js';
26
27
  import { createSlotlockNodeServer, } from './node-server.js';
@@ -114,7 +115,9 @@ const SERVED_OPERATIONS = new Set([
114
115
  'slotlock_get_free_busy',
115
116
  'slotlock_find_next_available',
116
117
  'slotlock_find_available_slots',
118
+ 'slotlock_find_available_ranges',
117
119
  'slotlock_hold_slots',
120
+ 'slotlock_hold_together',
118
121
  'slotlock_release_slots',
119
122
  'slotlock_book_together',
120
123
  'slotlock_book_pool',
@@ -929,6 +932,8 @@ export async function startSlotlockServer(config, log) {
929
932
  maxTenantResources: SERVE_MAX_TENANT_RESOURCES,
930
933
  // A granted approval's booking is made as an agent's would be (RFC 0004 A4).
931
934
  agentBackend,
935
+ // Holiday calendars by URL (RFC 0004 B1), never from a private address.
936
+ holidayFetch: createSlotlockGuardedFetch(),
932
937
  partners: createSlotlockPartnerStore(sql, {
933
938
  keys: createSlotlockApiKeyStore(sql),
934
939
  eraseTenant: (tenantRef) => store.eraseTenant({ tenantRef }),
@@ -975,7 +980,8 @@ export async function startSlotlockServer(config, log) {
975
980
  : {}),
976
981
  ...(config.dashboard.organizations === 'enabled' ? { teams: createSlotlockDashboardTeams(sql) } : {}),
977
982
  keys: createSlotlockApiKeyStore(sql),
978
- resources: createSlotlockDashboardResources(store, { agentBackend }),
983
+ // Holiday calendars by address (RFC 0004 B1), never from a private address.
984
+ resources: createSlotlockDashboardResources(store, { agentBackend, holidayFetch: createSlotlockGuardedFetch() }),
979
985
  state: createSlotlockDashboardState(sql),
980
986
  availability: config.availability,
981
987
  ...(config.usage ? { usage: true } : {}),
@@ -1037,6 +1043,8 @@ export async function startSlotlockServer(config, log) {
1037
1043
  SELECT slotlock.maintenance_tenants(${after}::text) AS tenant`),
1038
1044
  intervalMs: SERVE_MAINTENANCE_INTERVAL_MS,
1039
1045
  ...(config.usage ? { usage: true } : {}),
1046
+ // Holiday calendar subscriptions (RFC 0004 B1), never from a private address.
1047
+ holidayFetch: createSlotlockGuardedFetch(),
1040
1048
  // Counts only, never the tenant.
1041
1049
  onPass: (result) => {
1042
1050
  metrics?.maintenance(result);
@@ -1250,8 +1258,116 @@ function hoursLine(resource) {
1250
1258
  external_ref: resource.externalRef,
1251
1259
  timezone: resource.timezone,
1252
1260
  hours: resource.availabilityRules ?? null,
1261
+ inherited: resource.inheritedAvailability ?? null,
1253
1262
  })}\n`;
1254
1263
  }
1264
+ /**
1265
+ * `closures list|add|delete|import` (RFC 0004 B1): the tenant's closures and special days. Exit 0, 1
1266
+ * when a name, id or file is not found or refused, as JSON lines on stdout.
1267
+ */
1268
+ async function runClosures(config, io, subcommand, operands, given) {
1269
+ const sql = connect(config.databaseUrl, 1);
1270
+ const fail = (message) => {
1271
+ io.stderr.write(`slotlock: ${message}\n`);
1272
+ return 1;
1273
+ };
1274
+ try {
1275
+ const store = createSlotlockStore(sql);
1276
+ const tenantRef = config.tenantRef;
1277
+ const line = (exception) => io.stdout.write(`${JSON.stringify({
1278
+ id: exception.id,
1279
+ location: exception.location ?? null,
1280
+ resource_id: exception.resourceId ?? null,
1281
+ starts_on: exception.startsOn,
1282
+ ends_on: exception.endsOn,
1283
+ outcome: exception.outcome,
1284
+ hours: exception.hours ?? null,
1285
+ label: exception.label,
1286
+ source: exception.source,
1287
+ })}\n`);
1288
+ let scope = {};
1289
+ if (given.location !== undefined)
1290
+ scope = { location: given.location };
1291
+ if (given.resource !== undefined) {
1292
+ const resources = await tenantResources(store, tenantRef);
1293
+ const named = resources.find((resource) => resource.id === given.resource) ??
1294
+ resources.find((resource) => resource.externalRef === given.resource);
1295
+ if (!named)
1296
+ return fail('no resource with that reference or id in this tenant');
1297
+ scope = { resourceId: named.id };
1298
+ }
1299
+ try {
1300
+ if (subcommand === 'closures list') {
1301
+ const exceptions = await store.withTenant(tenantRef, (tenant) => tenant.listAvailabilityExceptions({
1302
+ tenantRef,
1303
+ from: new Date(Date.now() - 86_400_000).toISOString().slice(0, 10),
1304
+ limit: 1_000,
1305
+ }));
1306
+ for (const exception of exceptions)
1307
+ line(exception);
1308
+ return 0;
1309
+ }
1310
+ if (subcommand === 'closures delete') {
1311
+ const deleted = await store.withTenant(tenantRef, (tenant) => tenant.deleteAvailabilityException({ tenantRef, id: operands[0] }));
1312
+ if (!deleted)
1313
+ return fail('no closure with that id in this tenant');
1314
+ io.stdout.write(`${JSON.stringify({ deleted: true })}\n`);
1315
+ return 0;
1316
+ }
1317
+ if (subcommand === 'closures add') {
1318
+ let hours;
1319
+ if (given.open !== undefined) {
1320
+ const match = /^(\d{2}):(\d{2})-(\d{2}):(\d{2})$/.exec(given.open);
1321
+ if (!match)
1322
+ return fail('--open takes HH:MM-HH:MM, like 10:00-14:00');
1323
+ const [opens, closes] = [Number(match[1]) * 60 + Number(match[2]), Number(match[3]) * 60 + Number(match[4])];
1324
+ hours = [{ startMinutes: opens, durationMinutes: closes > opens ? closes - opens : closes + 1_440 - opens }];
1325
+ }
1326
+ const exception = await store.withTenant(tenantRef, (tenant) => tenant.createAvailabilityException({
1327
+ tenantRef,
1328
+ ...scope,
1329
+ startsOn: operands[0],
1330
+ ...(operands[1] !== undefined ? { endsOn: operands[1] } : {}),
1331
+ outcome: hours ? 'open' : 'closed',
1332
+ ...(hours ? { hours } : {}),
1333
+ label: given.label ?? '',
1334
+ }));
1335
+ line(exception);
1336
+ return 0;
1337
+ }
1338
+ // closures import <file> [--label <calendar name>]
1339
+ let text;
1340
+ try {
1341
+ text = await readFile(operands[0], 'utf8');
1342
+ }
1343
+ catch {
1344
+ return fail('that file cannot be read');
1345
+ }
1346
+ const read = await readSlotlockHolidayCalendar({
1347
+ icalendar: text,
1348
+ ...(given.label !== undefined ? { name: given.label } : {}),
1349
+ });
1350
+ const result = await store.withTenant(tenantRef, (tenant) => tenant.importHolidayCalendar({ tenantRef, ...scope, name: read.name, holidays: read.holidays, span: read.span }));
1351
+ io.stdout.write(`${JSON.stringify({ imported: result.imported, removed: result.removed, skipped: read.skipped, calendar: result.calendar.name })}\n`);
1352
+ return 0;
1353
+ }
1354
+ catch (error) {
1355
+ const code = error.code;
1356
+ if (code === 'invalid_availability_exception' ||
1357
+ code === 'invalid_holiday_calendar' ||
1358
+ code === 'location_not_found' ||
1359
+ code === 'resource_not_found' ||
1360
+ code === 'availability_exceptions_limit_reached' ||
1361
+ code === 'holiday_calendars_limit_reached') {
1362
+ return fail(error.message);
1363
+ }
1364
+ throw error;
1365
+ }
1366
+ }
1367
+ finally {
1368
+ await sql.end({ timeout: 5 });
1369
+ }
1370
+ }
1255
1371
  /** Every resource of the tenant in use (with `includeArchived`, every one), a page at a time. */
1256
1372
  async function tenantResources(store, tenantRef, includeArchived = false) {
1257
1373
  const all = [];
@@ -1273,14 +1389,22 @@ async function tenantResources(store, tenantRef, includeArchived = false) {
1273
1389
  * `hours show|set|clear`: print, set or clear (`rules` null) the hours of the tenant's resource
1274
1390
  * named by reference or id, or of every resource. Returns false when no resource has that name.
1275
1391
  */
1276
- async function runHours(config, out, target, rules) {
1392
+ async function runHours(config, out, target, rules, override = 'none') {
1277
1393
  const sql = connect(config.databaseUrl, 1);
1278
1394
  try {
1279
1395
  const store = createSlotlockStore(sql);
1280
1396
  const tenantRef = config.tenantRef;
1281
1397
  if (target === 'all') {
1282
- const updated = await store.withTenant(tenantRef, (tenant) => tenant.setTenantAvailability({ tenantRef, rules: rules ?? null }));
1283
- out.write(`${JSON.stringify({ updated })}\n`);
1398
+ // The tenant's hours, which every resource without its own inherits (RFC 0004 B2).
1399
+ const result = await store.withTenant(tenantRef, (tenant) => tenant.storeTenantAvailability({ tenantRef, rules: rules ?? null, overrideResources: override }));
1400
+ out.write(`${JSON.stringify({ inheriting: result.inheriting, own_hours: result.ownHours })}\n`);
1401
+ return true;
1402
+ }
1403
+ if (typeof target === 'object' && 'location' in target) {
1404
+ const result = await store.withTenant(tenantRef, (tenant) => tenant.setLocationAvailability({ tenantRef, location: target.location, rules: rules ?? null }));
1405
+ if (!result)
1406
+ return false;
1407
+ out.write(`${JSON.stringify({ location: target.location, inheriting: result.inheriting, own_hours: result.ownHours })}\n`);
1284
1408
  return true;
1285
1409
  }
1286
1410
  const resources = await tenantResources(store, tenantRef);
@@ -1298,6 +1422,8 @@ async function runHours(config, out, target, rules) {
1298
1422
  out.write(hoursLine(named));
1299
1423
  return true;
1300
1424
  }
1425
+ if (!('resource' in target))
1426
+ return false;
1301
1427
  const updated = await store.withTenant(tenantRef, (tenant) => tenant.setResourceAvailability({ tenantRef, id: named.id, rules }));
1302
1428
  if (!updated)
1303
1429
  return false;
@@ -1780,11 +1906,16 @@ async function maintainTenant(config, caps) {
1780
1906
  noShows: 0,
1781
1907
  waitlistOffers: 0,
1782
1908
  approvalsExpired: 0,
1909
+ bookingsCompleted: 0,
1910
+ remindersDue: 0,
1911
+ holidayCalendarsImported: 0,
1783
1912
  capped: true,
1784
1913
  passes: 0,
1785
1914
  };
1915
+ // Holiday calendar subscriptions by URL (RFC 0004 B1), never from a private address.
1916
+ const holidayFetch = createSlotlockGuardedFetch();
1786
1917
  while (sum.capped && sum.passes < MAINTAIN_MAX_PASSES) {
1787
- const pass = await maintainSlotlockTenant(store, config.tenantRef);
1918
+ const pass = await maintainSlotlockTenant(store, config.tenantRef, { holidayFetch });
1788
1919
  sum.passes += 1;
1789
1920
  sum.extended += pass.extended;
1790
1921
  sum.conflicts += pass.conflicts;
@@ -1795,6 +1926,9 @@ async function maintainTenant(config, caps) {
1795
1926
  sum.noShows += pass.noShows;
1796
1927
  sum.waitlistOffers += pass.waitlistOffers;
1797
1928
  sum.approvalsExpired += pass.approvalsExpired;
1929
+ sum.bookingsCompleted += pass.bookingsCompleted;
1930
+ sum.remindersDue += pass.remindersDue;
1931
+ sum.holidayCalendarsImported += pass.holidayCalendarsImported;
1798
1932
  // A capped pass that got nowhere met only series it cannot extend. While its roll stopped
1799
1933
  // before the series it had put back, the next pass meets new ones (and perhaps a healthy
1800
1934
  // one); once it reached them, another pass would meet them again.
@@ -1872,11 +2006,26 @@ Commands:
1872
2006
  resource delete <ref> Delete a resource and its history; refused while a booking, hold
1873
2007
  or event on it has not ended
1874
2008
  hours show [--resource <ref>]
1875
- Print each resource's bookable hours (null: the server's)
1876
- hours set <json> (--resource <ref> | --all)
1877
- Give one resource, or every one, weekly hours of its own; [] closes
1878
- hours clear (--resource <ref> | --all)
1879
- Hand one resource, or every one, back to the server's hours
2009
+ Print each resource's bookable hours: its own (null when none) and
2010
+ what it inherits from its location or the tenant
2011
+ hours set <json> (--resource <ref> | --location <ref> | --all [--override | --matching])
2012
+ Give one resource, one location or the tenant weekly hours; a
2013
+ resource without its own inherits its location's, then the
2014
+ tenant's, then the server's; --override also clears resources'
2015
+ own hours, --matching only those the same as the tenant's were
2016
+ or as these (copies 0.8.0 made); [] closes
2017
+ hours clear (--resource <ref> | --location <ref> | --all)
2018
+ Clear those hours, so they inherit from further up
2019
+ closures list Print the tenant's closures and special days from yesterday on
2020
+ closures add <first-day> [<last-day>] --label <text> [--open HH:MM-HH:MM]
2021
+ [--resource <ref> | --location <ref>]
2022
+ Close whole days (YYYY-MM-DD), or with --open give them those hours
2023
+ instead, everywhere or for one location or resource
2024
+ closures delete <id> Remove a closure
2025
+ closures import <file> [--resource <ref> | --location <ref>] [--label <calendar name>]
2026
+ Close the days an iCalendar file of holidays names (each all-day
2027
+ event, for the next two years); importing the calendar again (by its
2028
+ name: --label, else its own) updates them and removes those it dropped
1880
2029
  rules set <json> --resource <ref>
1881
2030
  Set what a booking on the resource must satisfy, like
1882
2031
  {"minNoticeMinutes":120,"slotStepMinutes":30,"turnaroundMinutes":30,"enforceHours":true}
@@ -2096,6 +2245,7 @@ export async function runSlotlockCli(argv, io) {
2096
2245
  const subcommand = command === 'resource' ||
2097
2246
  command === 'key' ||
2098
2247
  command === 'hours' ||
2248
+ command === 'closures' ||
2099
2249
  command === 'rules' ||
2100
2250
  command === 'tenant' ||
2101
2251
  command === 'plan' ||
@@ -2134,6 +2284,10 @@ export async function runSlotlockCli(argv, io) {
2134
2284
  'hours show': 1,
2135
2285
  'hours set': 2,
2136
2286
  'hours clear': 1,
2287
+ 'closures list': 1,
2288
+ 'closures add': 2,
2289
+ 'closures delete': 2,
2290
+ 'closures import': 2,
2137
2291
  'rules set': 2,
2138
2292
  'rules clear': 1,
2139
2293
  'partner create': 3,
@@ -2148,7 +2302,8 @@ export async function runSlotlockCli(argv, io) {
2148
2302
  'overage report': 1,
2149
2303
  'billing daily': 1,
2150
2304
  };
2151
- const expected = expectedOperands[subcommand];
2305
+ // closures add takes a last day or not.
2306
+ const expected = subcommand === 'closures add' && operands.length === 3 ? 3 : expectedOperands[subcommand];
2152
2307
  if (expected === undefined)
2153
2308
  return usage(`unknown command "${subcommand}"`);
2154
2309
  if (operands.length !== expected)
@@ -2208,10 +2363,32 @@ export async function runSlotlockCli(argv, io) {
2208
2363
  if (values.all && subcommand !== 'hours set' && subcommand !== 'hours clear') {
2209
2364
  return usage('--all applies only to hours set and hours clear');
2210
2365
  }
2366
+ if (values.override && subcommand !== 'hours set')
2367
+ return usage('--override applies only to hours set --all');
2368
+ if (values.matching && subcommand !== 'hours set')
2369
+ return usage('--matching applies only to hours set --all');
2370
+ if (values.override && values.matching)
2371
+ return usage('--override and --matching: give one');
2211
2372
  if (values.resource !== undefined &&
2212
2373
  !subcommand.startsWith('hours ') &&
2213
- !subcommand.startsWith('rules ')) {
2214
- return usage('--resource applies only to hours and rules commands');
2374
+ !subcommand.startsWith('rules ') &&
2375
+ !subcommand.startsWith('closures ')) {
2376
+ return usage('--resource applies only to hours, closures and rules commands');
2377
+ }
2378
+ if (values.location !== undefined && !subcommand.startsWith('hours ') && !subcommand.startsWith('closures ')) {
2379
+ return usage('--location applies only to hours and closures commands');
2380
+ }
2381
+ if (values.label !== undefined && subcommand !== 'closures add' && subcommand !== 'closures import') {
2382
+ return usage('--label applies only to closures add and closures import');
2383
+ }
2384
+ if (values.open !== undefined && subcommand !== 'closures add') {
2385
+ return usage('--open applies only to closures add');
2386
+ }
2387
+ if (subcommand.startsWith('closures ') && values.resource !== undefined && values.location !== undefined) {
2388
+ return usage(`${subcommand} takes --resource or --location, not both`);
2389
+ }
2390
+ if (subcommand === 'closures add' && (operands.length < 2 || operands.length > 3 || values.label === undefined)) {
2391
+ return usage('closures add needs <first-day> [<last-day>] --label <text>');
2215
2392
  }
2216
2393
  if (subcommand.startsWith('rules ') && values.resource === undefined) {
2217
2394
  return usage(`${subcommand} needs --resource <ref>`);
@@ -2228,9 +2405,13 @@ export async function runSlotlockCli(argv, io) {
2228
2405
  }
2229
2406
  }
2230
2407
  if ((subcommand === 'hours set' || subcommand === 'hours clear') &&
2231
- (values.resource === undefined) === (values.all !== true)) {
2232
- return usage(`${subcommand} needs --resource <ref> or --all, one of them`);
2408
+ [values.resource !== undefined, values.location !== undefined, values.all === true].filter(Boolean).length !== 1) {
2409
+ return usage(`${subcommand} needs --resource <ref>, --location <ref> or --all, one of them`);
2233
2410
  }
2411
+ if (values.override && values.all !== true)
2412
+ return usage('--override applies only to hours set --all');
2413
+ if (values.matching && values.all !== true)
2414
+ return usage('--matching applies only to hours set --all');
2234
2415
  let hours = null;
2235
2416
  if (subcommand === 'hours set') {
2236
2417
  let candidate;
@@ -2484,16 +2665,30 @@ export async function runSlotlockCli(argv, io) {
2484
2665
  case 'hours clear': {
2485
2666
  const target = values.all === true
2486
2667
  ? 'all'
2487
- : values.resource !== undefined
2488
- ? { resource: values.resource }
2489
- : 'show';
2490
- const found = await runHours(readSlotlockDatabaseConfig(io.env), io.stdout, target, subcommand === 'hours show' ? undefined : hours);
2668
+ : values.location !== undefined
2669
+ ? { location: values.location }
2670
+ : values.resource !== undefined
2671
+ ? { resource: values.resource }
2672
+ : 'show';
2673
+ const found = await runHours(readSlotlockDatabaseConfig(io.env), io.stdout, target, subcommand === 'hours show' ? undefined : hours, values.override === true ? 'all' : values.matching === true ? 'matching' : 'none');
2491
2674
  if (!found) {
2492
- io.stderr.write('slotlock: no resource with that reference or id in this tenant\n');
2675
+ io.stderr.write(values.location !== undefined
2676
+ ? 'slotlock: no location with that reference in this tenant\n'
2677
+ : 'slotlock: no resource with that reference or id in this tenant\n');
2493
2678
  return 1;
2494
2679
  }
2495
2680
  return 0;
2496
2681
  }
2682
+ case 'closures list':
2683
+ case 'closures add':
2684
+ case 'closures delete':
2685
+ case 'closures import':
2686
+ return runClosures(readSlotlockDatabaseConfig(io.env), io, subcommand, operands.slice(1), {
2687
+ ...(values.resource !== undefined ? { resource: values.resource } : {}),
2688
+ ...(values.location !== undefined ? { location: values.location } : {}),
2689
+ ...(values.label !== undefined ? { label: values.label } : {}),
2690
+ ...(values.open !== undefined ? { open: values.open } : {}),
2691
+ });
2497
2692
  case 'rules set':
2498
2693
  case 'rules clear': {
2499
2694
  const found = await runRules(readSlotlockDatabaseConfig(io.env), io.stdout, values.resource, bookingRules);
@@ -2594,7 +2789,12 @@ function parseCliArguments(argv) {
2594
2789
  month: { type: 'string' },
2595
2790
  event: { type: 'string' },
2596
2791
  resource: { type: 'string' },
2792
+ location: { type: 'string' },
2597
2793
  all: { type: 'boolean' },
2794
+ override: { type: 'boolean' },
2795
+ matching: { type: 'boolean' },
2796
+ label: { type: 'string' },
2797
+ open: { type: 'string' },
2598
2798
  reason: { type: 'string' },
2599
2799
  confirm: { type: 'string' },
2600
2800
  from: { type: 'string' },
@@ -3,7 +3,7 @@
3
3
  "schemaVersion": "1.0",
4
4
  "name": "Slotlock",
5
5
  "package": "slotlock",
6
- "version": "0.8.0",
6
+ "version": "0.9.0",
7
7
  "description": "Resource-first, deterministic calendar and availability engine for AI agents.",
8
8
  "intervalSemantics": {
9
9
  "boundary": "[)",