@awesomate/sdk 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -24,7 +24,7 @@
24
24
  * Docs: https://hub.awesomate.ai/docs/sdk/
25
25
  */
26
26
  /** This package's version, sent to the hub with every server call. */
27
- export declare const VERSION = "0.24.0";
27
+ export declare const VERSION = "0.25.0";
28
28
  /** Augmented by the generated awesomate.d.ts, so each kind's rows are typed. */
29
29
  export interface Kinds {
30
30
  }
@@ -205,9 +205,13 @@ export interface TaskCard {
205
205
  /** The Tasks board, as the account's token sees it (the owner, so `scope: 'all'` works). */
206
206
  export interface TaskBoard {
207
207
  scope: 'mine' | 'all';
208
+ /** Whether this person may ask for `scope: 'all'`: the owner only. */
209
+ canChooseScope: boolean;
208
210
  me: TaskPerson & {
209
211
  role: string;
210
212
  };
213
+ /** Whether this person may give tasks (everyone but view-only people). */
214
+ canGive: boolean;
211
215
  /** Who a task can be given to. */
212
216
  people: Array<TaskPerson & {
213
217
  me: boolean;
@@ -218,7 +222,10 @@ export interface TaskBoard {
218
222
  later: TaskCard[];
219
223
  /** With someone else. */
220
224
  waiting: TaskCard[];
221
- /** Recently decided, for the record. */
225
+ /**
226
+ * Recently decided, for the record. `canBringBack` is true for a given task this person may open
227
+ * again with bringBack(): the number after `task:` in its key is the task's id.
228
+ */
222
229
  decided: Array<{
223
230
  key: string;
224
231
  kind: string;
@@ -226,7 +233,49 @@ export interface TaskBoard {
226
233
  text: string;
227
234
  by: TaskPerson | null;
228
235
  at: string;
236
+ canBringBack: boolean;
229
237
  }>;
238
+ /**
239
+ * Places the board could not read just now, in the owner's words. When it is not empty the
240
+ * board may be missing cards from them: say so rather than treating the board as complete.
241
+ */
242
+ unavailable: string[];
243
+ /** Whether anyone besides the owner is on the account. */
244
+ hasTeam: boolean;
245
+ }
246
+ /** One step in a task's record, in order. */
247
+ export interface TaskRecordStep {
248
+ at: string;
249
+ /** What happened, in a sentence. */
250
+ text: string;
251
+ /** Where it happened (email, Telegram), when that was not the hub; otherwise null. */
252
+ place: string | null;
253
+ /** How long since the step before, when it is worth showing ("29 min later"). */
254
+ gap: string | null;
255
+ tone: 'start' | 'move' | 'seen' | 'answer' | 'end';
256
+ }
257
+ /** What happened to a given task, from tasks.record(): who gave it, who passed it on, who closed it, and how long each person had it. */
258
+ export interface TaskRecord {
259
+ kind: 'task';
260
+ title: string;
261
+ state: string;
262
+ steps: TaskRecordStep[];
263
+ timings: {
264
+ /** From given to done or dropped; null while it is open. */
265
+ totalMs: number | null;
266
+ total: string | null;
267
+ /** From reaching its first person to that person's first look; null when they have not looked. */
268
+ untilFirstSeenMs: number | null;
269
+ untilFirstSeen: string | null;
270
+ /** Time with each person, in order of first holding it. */
271
+ withPeople: Array<{
272
+ name: string;
273
+ ms: number;
274
+ text: string;
275
+ }>;
276
+ /** How many times it moved to someone else after it was first given. */
277
+ handoffs: number;
278
+ };
230
279
  }
231
280
  /** A task to give: to someone on the account by email, or to the owner when `forEmail` is left out. */
232
281
  export interface GiveTask {
@@ -251,6 +300,17 @@ export interface FileEntry {
251
300
  /** For a file in public/: the address anyone with it can open, with no login. Otherwise null. */
252
301
  public_url: string | null;
253
302
  }
303
+ /** Something in the Files trash, from files.trash(). */
304
+ export interface TrashEntry {
305
+ /** Where it sits in the trash. */
306
+ path: string;
307
+ /** What it was called before it was removed. */
308
+ name: string;
309
+ type: 'file' | 'dir';
310
+ bytes: number;
311
+ /** When it was removed, or null when the trash name does not say. */
312
+ deleted_at: string | null;
313
+ }
254
314
  /** How much of the plan's Files space is used. */
255
315
  export interface FilesUsage {
256
316
  used_bytes: number;
@@ -393,6 +453,23 @@ export declare class AwesomateClient {
393
453
  remove: (path: string) => Promise<{
394
454
  trashPath: string;
395
455
  }>;
456
+ /** What is in the trash, and how many bytes it holds. Needs files:read. */
457
+ trash: () => Promise<{
458
+ entries: TrashEntry[];
459
+ total_bytes: number;
460
+ }>;
461
+ /**
462
+ * Erase what is in the trash, for good: the only call that deletes a file outright. With
463
+ * `olderThanDays` (1 to 3650), only what was removed longer ago than that. Needs files:write.
464
+ * An automation account that cannot empty its trash yet answers serverCode trash_empty_unavailable.
465
+ */
466
+ emptyTrash: (options?: {
467
+ olderThanDays?: number;
468
+ }) => Promise<{
469
+ removed: number;
470
+ bytes: number;
471
+ files: number;
472
+ }>;
396
473
  };
397
474
  private tasksCall;
398
475
  /**
@@ -422,16 +499,122 @@ export declare class AwesomateClient {
422
499
  done: (id: number | string) => Promise<void>;
423
500
  /** Take a given task off the board without doing it: whoever wrote it, or the owner. */
424
501
  drop: (id: number | string) => Promise<void>;
502
+ /**
503
+ * Open a done or dropped task again, with the person who had it. They get an email unless
504
+ * they are the one bringing it back. Who may: see `decided[].canBringBack` on the board.
505
+ */
506
+ bringBack: (id: number | string) => Promise<{
507
+ emailed: boolean;
508
+ }>;
509
+ /**
510
+ * What happened to a given task, in order: who gave it, who passed it on, who closed it and
511
+ * brought it back, and how long each person had it. The owner reads any task's record; anyone
512
+ * else only one they were part of.
513
+ */
514
+ record: (id: number | string) => Promise<TaskRecord>;
425
515
  /** Put a card off until a time (at most 90 days), for this person only. */
426
516
  notNow: (key: string, until: Date | string) => Promise<{
427
517
  until: string;
428
518
  }>;
519
+ /** Bring a card put off with notNow() back now, for this person. */
520
+ cancelNotNow: (key: string) => Promise<void>;
429
521
  /** Hand a card to someone on the account who can act on it. */
430
522
  passOn: (key: string, toEmail: string, reason?: string) => Promise<{
431
523
  holder: TaskPerson;
432
524
  emailed: boolean;
433
525
  }>;
434
526
  };
527
+ /**
528
+ * The account's bookings, for the business's own staff screens: the list with each customer,
529
+ * one booking, the open times, and booking, cancelling, moving and recording an outcome on
530
+ * anyone's behalf. Needs the account's token (never an app key) and Bookings on the account.
531
+ * Reading needs crm:read, on every plan; changing needs crm:write, Support Plus and above.
532
+ * Booking, moving and cancelling email the customer and the calendar's notice address, unless
533
+ * `notify: false` (say a booking taken by phone that you confirm yourself). For visitors who are
534
+ * not signed in, use createBookingsClient() instead.
535
+ *
536
+ * @example
537
+ * const today = await db.bookings.list({ from: '2026-10-08T00:00:00+11:00', to: '2026-10-09T00:00:00+11:00', status: 'confirmed' });
538
+ */
539
+ readonly bookings: {
540
+ /**
541
+ * Bookings that start in a window, soonest first, each with its customer. The window is from a
542
+ * day ago for 31 days unless told otherwise; `limit` is 1 to 500 (default 200).
543
+ */
544
+ list: (options?: {
545
+ from?: Date | string;
546
+ to?: Date | string;
547
+ status?: StaffBooking["status"];
548
+ calendar?: string;
549
+ limit?: number;
550
+ }) => Promise<StaffBooking[]>;
551
+ /** One booking by its id, or null when there is none (an id that is not a booking id never reaches the hub). */
552
+ get: (id: string) => Promise<StaffBooking | null>;
553
+ /**
554
+ * The times a service can be booked, from now for two weeks unless told otherwise (at most 62
555
+ * days at once). The same open times customers see; book() with `outsideHours` goes beyond them.
556
+ */
557
+ openTimes: (service: string, options?: {
558
+ from?: Date | string;
559
+ to?: Date | string;
560
+ calendar?: string;
561
+ seats?: number;
562
+ }) => Promise<OpenTime[]>;
563
+ /**
564
+ * Book a time for a customer named by email (found in Contacts, or added; consent is never
565
+ * changed). Switches Bookings on if it was not. A time that is not open is refused with code
566
+ * `conflict` and `field` saying why: not_open, slot_taken, too_many_seats, session_full or
567
+ * day_full. Pass `idempotencyKey` so a retry returns the same booking.
568
+ */
569
+ book: (request: StaffBookingRequest) => Promise<{
570
+ bookingId: string;
571
+ created: boolean;
572
+ startsAt: string;
573
+ endsAt: string;
574
+ }>;
575
+ /** Cancel a booking. A booking already cancelled answers `cancelled: false`. */
576
+ cancel: (id: string, options?: {
577
+ reason?: string;
578
+ notify?: boolean;
579
+ }) => Promise<{
580
+ bookingId: string;
581
+ cancelled: boolean;
582
+ status: string | null;
583
+ }>;
584
+ /**
585
+ * Move a confirmed booking to a new start, on the same calendar or another (`calendar`). A time
586
+ * that is not open is refused with code `conflict` (field not_open, slot_taken, session_full or
587
+ * day_full) unless `outsideHours`; a booking that is not confirmed, or a time that has passed,
588
+ * with code `validation`.
589
+ */
590
+ move: (id: string, startsAt: Date | string, options?: {
591
+ calendar?: string;
592
+ outsideHours?: boolean;
593
+ notify?: boolean;
594
+ }) => Promise<{
595
+ bookingId: string;
596
+ startsAt: string;
597
+ endsAt: string;
598
+ calendar: string;
599
+ }>;
600
+ /** Record how a booking went: the customer came (`completed`) or did not (`no_show`). Sends no email. */
601
+ outcome: (id: string, status: "completed" | "no_show") => Promise<void>;
602
+ };
603
+ /**
604
+ * Tell Contacts something happened to someone ("job_paid", "quote_sent"), so any email series
605
+ * waiting for that event starts or stops for them. The person is found by email, or added; it
606
+ * never signs anyone up for email or changes their consent. Sending the same `key` twice records
607
+ * the event once (`duplicate: true`), so pass your own id for it. `record` names the quote, job or
608
+ * booking it is about. An event more than two days old is kept as a fact a series reads, never
609
+ * a start. Needs the account's token with crm:write (Support Plus and above), never an app key.
610
+ * The answer is the same whether or not the address belongs to someone erased from Contacts.
611
+ *
612
+ * @example
613
+ * await db.recordEvent({ event: 'job_paid', email: 'sam@example.com', record: 'job-1042', key: 'xero:INV-1042' });
614
+ */
615
+ recordEvent(event: SeriesEvent): Promise<{
616
+ duplicate: boolean;
617
+ }>;
435
618
  /** The kinds this token can read, their columns, operators and examples. */
436
619
  schema(): Promise<{
437
620
  kinds: Array<{
@@ -466,7 +649,10 @@ export declare class AwesomateClient {
466
649
  queryAll<K extends KindName, S extends keyof RowOf<K> = keyof RowOf<K>>(kind: K, options?: Omit<QueryOptions<RowOf<K>, S>, 'after'> & {
467
650
  maxRows?: number;
468
651
  }): AsyncGenerator<Pick<RowOf<K>, S>>;
469
- /** One row by id, or null when there is none this token can read. */
652
+ /**
653
+ * One row by id, or null when there is none this token can read. Ids are UUIDs: anything else
654
+ * names no row, so it answers null without asking the hub.
655
+ */
470
656
  get<K extends KindName>(kind: K, id: string, options?: {
471
657
  tz?: string;
472
658
  }): Promise<RowOf<K> | null>;
@@ -537,6 +723,18 @@ export declare class AwesomateClient {
537
723
  * gone 30 days unused, and not before 2026-11-06.
538
724
  */
539
725
  businessMapV1(): Promise<BusinessMapV1>;
726
+ /**
727
+ * The business's long documents that exist (brand guide, voice guide, brand from the website,
728
+ * business summary): kind, version, length and where each came from, without the text. Needs
729
+ * the account's token (hosting:read, every plan), never an app key.
730
+ */
731
+ businessDocuments(): Promise<BusinessDocumentSummary[]>;
732
+ /**
733
+ * One of the business's documents with its text (the current version), or null when the
734
+ * business has none of that kind yet. The text is the owner's own: use it for their business
735
+ * (agent instructions, site copy in their voice), never show it to anyone else.
736
+ */
737
+ businessDocument(kind: BusinessDocumentKind): Promise<BusinessDocument | null>;
540
738
  /** Details waiting for the owner's yes on Your business: from the website, the account, a team member or a file. */
541
739
  businessSuggestions(): Promise<BusinessSuggestion[]>;
542
740
  /** Grouped numbers (counts, sums) in the Business Data API's shape. */
@@ -1021,6 +1219,44 @@ export interface BusinessSuggestion {
1021
1219
  sourceRef: string | null;
1022
1220
  recordedAt: string;
1023
1221
  }
1222
+ /** The business's long documents, by kind. */
1223
+ export type BusinessDocumentKind = 'brand_guide' | 'voice_guide' | 'website_brand' | 'business_summary';
1224
+ /** One of the business's documents, without its text (businessDocuments()). */
1225
+ export interface BusinessDocumentSummary {
1226
+ kind: BusinessDocumentKind;
1227
+ /** What the owner sees it called in the hub ("Voice and style guide"). */
1228
+ label: string;
1229
+ /** Goes up by one each time it is saved again. */
1230
+ version: number;
1231
+ /** How long the text is, in characters. */
1232
+ chars: number;
1233
+ /** Where this version came from: the owner, the onboarding agent, a chat brought in, a file. */
1234
+ sourceKind: string;
1235
+ recordedAt: string;
1236
+ }
1237
+ /** One of the business's documents with its text (businessDocument()). */
1238
+ export interface BusinessDocument extends BusinessDocumentSummary {
1239
+ /** The document itself, usually Markdown. The owner's own words. */
1240
+ body: string;
1241
+ }
1242
+ /**
1243
+ * What happened to someone, for recordEvent(). `event` is lower case: letters, digits and
1244
+ * `_ . : -`, up to 64 characters ("job_paid", "quote_sent").
1245
+ */
1246
+ export interface SeriesEvent {
1247
+ event: string;
1248
+ /** Whose event it is. Found in Contacts, or added. */
1249
+ email: string;
1250
+ /** The quote, job or booking it is about (1 to 120 characters). A series that runs per record keys on it. */
1251
+ record?: string;
1252
+ /** When it happened. Default now. */
1253
+ occurredAt?: Date | string;
1254
+ /** Your own id for the event (1 to 200 characters). Sending the same key again records it once. */
1255
+ key?: string;
1256
+ /** Used only when the person is added to Contacts by this event. */
1257
+ firstName?: string;
1258
+ lastName?: string;
1259
+ }
1024
1260
  /** One attribute (column) of a kind. */
1025
1261
  export interface AttributeSpec {
1026
1262
  key: string;
@@ -1031,6 +1267,20 @@ export interface AttributeSpec {
1031
1267
  sensitivity?: 'ordinary' | 'personal' | 'sensitive';
1032
1268
  /** Default false. Only readable attributes reach query(), get() and the generated types. */
1033
1269
  readable_by_ai?: boolean;
1270
+ /**
1271
+ * Which of the app's roles see this attribute (up to 10, lower case). Left out or null: everyone
1272
+ * who can read the record. An empty list: none of the app's people.
1273
+ */
1274
+ visible_to?: string[] | null;
1275
+ }
1276
+ /**
1277
+ * Who among an app's signed-in people may read and change a kind's records, by role. Each rule is
1278
+ * `all`, `none`, `own` (records they created) or `linked:<link>` (records linked to them, such as
1279
+ * `linked:customer` or `linked:job.customer`); join several with `|`. A role left out gets none.
1280
+ */
1281
+ export interface KindAccess {
1282
+ read?: Record<string, string>;
1283
+ write?: Record<string, string>;
1034
1284
  }
1035
1285
  /** A kind to define: a table an app keeps. */
1036
1286
  export interface KindSpec {
@@ -1046,16 +1296,22 @@ export interface KindSpec {
1046
1296
  to_label?: string;
1047
1297
  required?: boolean;
1048
1298
  }>;
1299
+ /** The app's access rules, set with the kind. Until a kind has rules, owner and staff can do everything with it and members nothing. */
1300
+ access?: KindAccess;
1049
1301
  }
1050
1302
  /** A kind as the account has defined it. */
1051
- export interface KindDescription extends Omit<KindSpec, 'attributes'> {
1303
+ export interface KindDescription extends Omit<KindSpec, 'attributes' | 'access'> {
1052
1304
  storage: 'plain' | 'tracked';
1053
1305
  attributes: Array<Required<Pick<AttributeSpec, 'key' | 'label' | 'type' | 'required' | 'sensitivity' | 'readable_by_ai'>> & {
1054
1306
  choices: string[] | null;
1307
+ visible_to: string[] | null;
1055
1308
  }>;
1309
+ /** The kind's access rules. A kind with none set reads owner and staff `all`, for reading and writing. */
1310
+ access: KindAccess;
1056
1311
  views: {
1057
1312
  all: string;
1058
1313
  for_agents: string;
1314
+ for_app: string;
1059
1315
  };
1060
1316
  }
1061
1317
  /** Options for write(). */
@@ -1130,6 +1386,8 @@ export interface RecipeSpec {
1130
1386
  export interface RecipeDescription extends Required<Omit<RecipeSpec, 'description' | 'params'>> {
1131
1387
  description: string | null;
1132
1388
  params: ParamSpec[];
1389
+ /** The app roles that may run it from the app (empty: only your server, with the account's token or an app key). The owner sets them in the hub or with Claude Code. */
1390
+ run_by: string[];
1133
1391
  updated_at: string;
1134
1392
  }
1135
1393
  /** What a recipe run wrote. */
@@ -1336,7 +1594,10 @@ export declare class AwesomateAppClient {
1336
1594
  queryAll<K extends KindName, S extends keyof RowOf<K> = keyof RowOf<K>>(kind: K, options?: Omit<QueryOptions<RowOf<K>, S>, 'after'> & {
1337
1595
  maxRows?: number;
1338
1596
  }): AsyncGenerator<Pick<RowOf<K>, S>>;
1339
- /** One record by id, or null when there is none this user may read. */
1597
+ /**
1598
+ * One record by id, or null when there is none this user may read. Ids are UUIDs: anything
1599
+ * else names no record, so it answers null without asking the hub.
1600
+ */
1340
1601
  get<K extends KindName>(kind: K, id: string): Promise<RowOf<K> | null>;
1341
1602
  /** Create a record (returns its id) or change one with options.id, inside the kind's write rule for this user. */
1342
1603
  write<K extends KindName>(kind: K, data: Partial<RowOf<K>> | Record<string, unknown>, options?: WriteOptions): Promise<string>;
@@ -1474,6 +1735,8 @@ export interface BookableService {
1474
1735
  /** The price as the business wrote it. Shown, never charged. */
1475
1736
  price_text: string;
1476
1737
  location: string;
1738
+ /** How many hours before the start a customer can still cancel or move online. */
1739
+ cancel_cutoff_hours: number;
1477
1740
  intake: BookingQuestion[];
1478
1741
  calendars: Array<{
1479
1742
  key: string;
@@ -1538,6 +1801,66 @@ export interface ManagedBooking {
1538
1801
  canCancel: boolean;
1539
1802
  canMove: boolean;
1540
1803
  changesCloseAt: string;
1804
+ /** The price as the business wrote it. Shown, never charged. */
1805
+ priceText: string;
1806
+ }
1807
+ /** How the business looks, for a booking page in its colours. Each is null when the business has not set it. */
1808
+ export interface BookingPageLook {
1809
+ /** The business's website (https). */
1810
+ website: string | null;
1811
+ /** Its logo's address (https). */
1812
+ logo: string | null;
1813
+ /** Its main colour, as #rrggbb. */
1814
+ colour: string | null;
1815
+ }
1816
+ /** A booking as the business's staff see it (db.bookings), with its customer. */
1817
+ export interface StaffBooking {
1818
+ bookingId: string;
1819
+ status: 'confirmed' | 'cancelled' | 'completed' | 'no_show';
1820
+ /** The service's key, and its name when the booking was made. */
1821
+ service: string;
1822
+ serviceName: string | null;
1823
+ /** The calendar's key, and its name when the booking was made. */
1824
+ calendar: string;
1825
+ calendarName: string | null;
1826
+ startsAt: string;
1827
+ endsAt: string;
1828
+ seats: number;
1829
+ /** How it was made: website, hub, phone, agent or api. */
1830
+ source: string | null;
1831
+ /** The customer's answers to the service's questions, by question key. */
1832
+ answers: Record<string, string | string[]> | null;
1833
+ /** The customer's contact id in Contacts. */
1834
+ contactId: string;
1835
+ /** The customer, or null when they have since been removed from Contacts. */
1836
+ customer: {
1837
+ name: string | null;
1838
+ email: string | null;
1839
+ phone: string | null;
1840
+ } | null;
1841
+ createdAt: string;
1842
+ cancelledAt: string | null;
1843
+ }
1844
+ /** What db.bookings.book() needs. Take `startsAt` and `calendar` from an OpenTime. */
1845
+ export interface StaffBookingRequest {
1846
+ service: string;
1847
+ calendar: string;
1848
+ startsAt: string;
1849
+ /** The customer, found in Contacts by email or added. */
1850
+ email: string;
1851
+ firstName?: string;
1852
+ lastName?: string;
1853
+ phone?: string;
1854
+ /** Places, for a class. Default 1. */
1855
+ seats?: number;
1856
+ /** Answers to the service's questions, by question key. */
1857
+ answers?: Record<string, string | string[]>;
1858
+ /** One per booking, kept across retries, so a retry returns the same booking. */
1859
+ idempotencyKey?: string;
1860
+ /** Book outside the calendar's hours and notice. Overlap, seats and the daily limit still apply. */
1861
+ outsideHours?: boolean;
1862
+ /** false: no email to the customer (the calendar's notice address still hears). Default true. */
1863
+ notify?: boolean;
1541
1864
  }
1542
1865
  /** Options for createBookingsClient(). */
1543
1866
  export interface BookingsClientOptions {
@@ -1579,13 +1902,19 @@ export declare class AwesomateBookingsClient {
1579
1902
  }): Promise<OpenTime[]>;
1580
1903
  /**
1581
1904
  * Book a time for a visitor. They get an email with an invite and a link to change or cancel;
1582
- * the business gets a notice. A time taken since you listed it is refused with code `conflict`
1583
- * and `field` saying why (not_open, slot_taken, session_full, day_full): list the times again.
1905
+ * the business gets a notice. A refusal has code `conflict` and `field` saying why:
1906
+ * - `not_open`, `slot_taken`, `too_many_seats`, `session_full`, `day_full`: the time is not
1907
+ * free (taken since you listed it, or not enough places left): list the times again;
1908
+ * - `too_many_open`: this email already has three bookings coming up;
1909
+ * - `monthly_limit`: the business has taken its plan's online bookings for the month.
1910
+ *
1911
+ * Show `personMessage` to the visitor for the last two.
1584
1912
  */
1585
1913
  book(request: BookingRequest): Promise<BookingResult>;
1586
- /** One booking, from the token in its manage link (manageTokenFrom()). */
1914
+ /** One booking, from the token in its manage link (manageTokenFrom()), with how the business looks so the page can match it. */
1587
1915
  booking(manageToken: string): Promise<{
1588
1916
  business: string;
1917
+ look: BookingPageLook;
1589
1918
  booking: ManagedBooking;
1590
1919
  }>;
1591
1920
  /** The times a booking could move to, leaving its own time out. */
@@ -1593,11 +1922,18 @@ export declare class AwesomateBookingsClient {
1593
1922
  from?: Date | string;
1594
1923
  to?: Date | string;
1595
1924
  }): Promise<OpenTime[]>;
1596
- /** Cancel a booking from its manage link. A second cancel answers cancelled: false. */
1925
+ /**
1926
+ * Cancel a booking from its manage link. A second cancel answers cancelled: false. Once changes
1927
+ * have closed (`canCancel` false) it is refused with code `conflict`, field `too_late`.
1928
+ */
1597
1929
  cancel(manageToken: string, reason?: string): Promise<{
1598
1930
  cancelled: boolean;
1599
1931
  }>;
1600
- /** Move a booking to a time from openTimesToMove(). */
1932
+ /**
1933
+ * Move a booking to a time from openTimesToMove(). Refused with code `conflict` and field
1934
+ * `too_late` once changes have closed, or `not_open` (or another of book()'s reasons) when the
1935
+ * time is no longer free.
1936
+ */
1601
1937
  move(manageToken: string, startsAt: string): Promise<{
1602
1938
  startsAt: string;
1603
1939
  endsAt: string;
package/dist/index.js CHANGED
@@ -24,8 +24,10 @@
24
24
  * Docs: https://hub.awesomate.ai/docs/sdk/
25
25
  */
26
26
  /** This package's version, sent to the hub with every server call. */
27
- export const VERSION = '0.24.0';
27
+ export const VERSION = '0.25.0';
28
28
  const DEFAULT_BASE = 'https://hub.awesomate.ai';
29
+ /** Every record and contact id is a UUID; anything else can name no row. */
30
+ const RECORD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
29
31
  const ERROR_CODES = ['unauthenticated', 'forbidden', 'not_found', 'validation', 'consent_blocked', 'rate_limited', 'conflict', 'unavailable'];
30
32
  /**
31
33
  * Every failure from the hub. `message` is for you, the developer; `personMessage`, when the hub
@@ -229,6 +231,24 @@ export class AwesomateClient {
229
231
  },
230
232
  /** Move to the trash. Not erased, and no longer counted toward the Files space. */
231
233
  remove: async (path) => this.filesCall('DELETE', `/api/files?path=${encodeURIComponent(path)}`),
234
+ /** What is in the trash, and how many bytes it holds. Needs files:read. */
235
+ trash: async () => {
236
+ const r = await this.filesCall('GET', '/api/files/trash');
237
+ return { entries: r.entries, total_bytes: r.total_bytes };
238
+ },
239
+ /**
240
+ * Erase what is in the trash, for good: the only call that deletes a file outright. With
241
+ * `olderThanDays` (1 to 3650), only what was removed longer ago than that. Needs files:write.
242
+ * An automation account that cannot empty its trash yet answers serverCode trash_empty_unavailable.
243
+ */
244
+ emptyTrash: async (options = {}) => {
245
+ const days = options.olderThanDays;
246
+ if (days !== undefined && (!Number.isInteger(days) || days < 1 || days > 3650)) {
247
+ throw new AwesomateError('validation', 'olderThanDays is a whole number of days from 1 to 3650, or leave it out to empty everything.', 0, 'olderThanDays');
248
+ }
249
+ const r = await this.filesCall('POST', '/api/files/trash/empty', days === undefined ? {} : { older_than_days: days });
250
+ return { removed: r.removed, bytes: r.bytes, files: r.files };
251
+ },
232
252
  };
233
253
  tasksCall(method, path, body) {
234
254
  if (this.appKey) {
@@ -265,6 +285,20 @@ export class AwesomateClient {
265
285
  drop: async (id) => {
266
286
  await this.tasksCall('POST', `/api/my-tasks/tasks/${encodeURIComponent(String(id))}/drop`);
267
287
  },
288
+ /**
289
+ * Open a done or dropped task again, with the person who had it. They get an email unless
290
+ * they are the one bringing it back. Who may: see `decided[].canBringBack` on the board.
291
+ */
292
+ bringBack: async (id) => {
293
+ const r = await this.tasksCall('POST', `/api/my-tasks/tasks/${encodeURIComponent(String(id))}/reopen`);
294
+ return { emailed: r.emailed };
295
+ },
296
+ /**
297
+ * What happened to a given task, in order: who gave it, who passed it on, who closed it and
298
+ * brought it back, and how long each person had it. The owner reads any task's record; anyone
299
+ * else only one they were part of.
300
+ */
301
+ record: (id) => this.tasksCall('GET', `/api/my-tasks/tasks/${encodeURIComponent(String(id))}/record`),
268
302
  /** Put a card off until a time (at most 90 days), for this person only. */
269
303
  notNow: async (key, until) => {
270
304
  const r = await this.tasksCall('POST', `/api/my-tasks/items/${encodeURIComponent(key)}/not-now`, {
@@ -272,6 +306,10 @@ export class AwesomateClient {
272
306
  });
273
307
  return { until: r.until };
274
308
  },
309
+ /** Bring a card put off with notNow() back now, for this person. */
310
+ cancelNotNow: async (key) => {
311
+ await this.tasksCall('DELETE', `/api/my-tasks/items/${encodeURIComponent(key)}/not-now`);
312
+ },
275
313
  /** Hand a card to someone on the account who can act on it. */
276
314
  passOn: async (key, toEmail, reason) => {
277
315
  const r = await this.tasksCall('POST', `/api/my-tasks/items/${encodeURIComponent(key)}/pass-on`, {
@@ -280,6 +318,126 @@ export class AwesomateClient {
280
318
  return { holder: r.holder, emailed: r.emailed };
281
319
  },
282
320
  };
321
+ /**
322
+ * The account's bookings, for the business's own staff screens: the list with each customer,
323
+ * one booking, the open times, and booking, cancelling, moving and recording an outcome on
324
+ * anyone's behalf. Needs the account's token (never an app key) and Bookings on the account.
325
+ * Reading needs crm:read, on every plan; changing needs crm:write, Support Plus and above.
326
+ * Booking, moving and cancelling email the customer and the calendar's notice address, unless
327
+ * `notify: false` (say a booking taken by phone that you confirm yourself). For visitors who are
328
+ * not signed in, use createBookingsClient() instead.
329
+ *
330
+ * @example
331
+ * const today = await db.bookings.list({ from: '2026-10-08T00:00:00+11:00', to: '2026-10-09T00:00:00+11:00', status: 'confirmed' });
332
+ */
333
+ bookings = {
334
+ /**
335
+ * Bookings that start in a window, soonest first, each with its customer. The window is from a
336
+ * day ago for 31 days unless told otherwise; `limit` is 1 to 500 (default 200).
337
+ */
338
+ list: async (options = {}) => {
339
+ const q = new URLSearchParams();
340
+ const from = iso(options.from);
341
+ const to = iso(options.to);
342
+ if (from)
343
+ q.set('from', from);
344
+ if (to)
345
+ q.set('to', to);
346
+ if (options.status)
347
+ q.set('status', options.status);
348
+ if (options.calendar)
349
+ q.set('calendar', options.calendar);
350
+ if (options.limit)
351
+ q.set('limit', String(options.limit));
352
+ const qs = q.toString();
353
+ const r = await this.request('GET', `/api/my-crm/v1/bookings${qs ? `?${qs}` : ''}`, undefined, false);
354
+ return r.bookings.map(staffBooking);
355
+ },
356
+ /** One booking by its id, or null when there is none (an id that is not a booking id never reaches the hub). */
357
+ get: async (id) => {
358
+ if (!RECORD_ID.test(String(id)))
359
+ return null;
360
+ try {
361
+ const r = await this.request('GET', `/api/my-crm/v1/bookings/${encodeURIComponent(String(id))}`, undefined, false);
362
+ return staffBooking(r.booking);
363
+ }
364
+ catch (err) {
365
+ if (err instanceof AwesomateError && err.code === 'not_found')
366
+ return null;
367
+ throw err;
368
+ }
369
+ },
370
+ /**
371
+ * The times a service can be booked, from now for two weeks unless told otherwise (at most 62
372
+ * days at once). The same open times customers see; book() with `outsideHours` goes beyond them.
373
+ */
374
+ openTimes: async (service, options = {}) => {
375
+ const q = new URLSearchParams({ service });
376
+ const from = iso(options.from);
377
+ const to = iso(options.to);
378
+ if (from)
379
+ q.set('from', from);
380
+ if (to)
381
+ q.set('to', to);
382
+ if (options.calendar)
383
+ q.set('calendar', options.calendar);
384
+ if (options.seats)
385
+ q.set('seats', String(options.seats));
386
+ return (await this.request('GET', `/api/my-crm/v1/bookings/open-times?${q}`, undefined, false)).times;
387
+ },
388
+ /**
389
+ * Book a time for a customer named by email (found in Contacts, or added; consent is never
390
+ * changed). Switches Bookings on if it was not. A time that is not open is refused with code
391
+ * `conflict` and `field` saying why: not_open, slot_taken, too_many_seats, session_full or
392
+ * day_full. Pass `idempotencyKey` so a retry returns the same booking.
393
+ */
394
+ book: async (request) => {
395
+ const r = await this.request('POST', '/api/my-crm/v1/bookings', {
396
+ service: request.service, calendar: request.calendar, starts_at: request.startsAt, email: request.email,
397
+ first_name: request.firstName, last_name: request.lastName, phone: request.phone, seats: request.seats, answers: request.answers,
398
+ idempotency_key: request.idempotencyKey, outside_hours: request.outsideHours, notify: request.notify,
399
+ }, false);
400
+ return { bookingId: r.booking_id, created: r.created, startsAt: r.starts_at, endsAt: r.ends_at };
401
+ },
402
+ /** Cancel a booking. A booking already cancelled answers `cancelled: false`. */
403
+ cancel: async (id, options = {}) => {
404
+ const r = await this.request('POST', `/api/my-crm/v1/bookings/${encodeURIComponent(String(id))}/cancel`, { reason: options.reason, notify: options.notify }, false);
405
+ return { bookingId: r.booking_id, cancelled: r.cancelled, status: r.status };
406
+ },
407
+ /**
408
+ * Move a confirmed booking to a new start, on the same calendar or another (`calendar`). A time
409
+ * that is not open is refused with code `conflict` (field not_open, slot_taken, session_full or
410
+ * day_full) unless `outsideHours`; a booking that is not confirmed, or a time that has passed,
411
+ * with code `validation`.
412
+ */
413
+ move: async (id, startsAt, options = {}) => {
414
+ const r = await this.request('POST', `/api/my-crm/v1/bookings/${encodeURIComponent(String(id))}/move`, { starts_at: iso(startsAt), calendar: options.calendar, outside_hours: options.outsideHours, notify: options.notify }, false);
415
+ return { bookingId: r.booking_id, startsAt: r.starts_at, endsAt: r.ends_at, calendar: r.calendar };
416
+ },
417
+ /** Record how a booking went: the customer came (`completed`) or did not (`no_show`). Sends no email. */
418
+ outcome: async (id, status) => {
419
+ await this.request('POST', `/api/my-crm/v1/bookings/${encodeURIComponent(String(id))}/outcome`, { status }, false);
420
+ },
421
+ };
422
+ /**
423
+ * Tell Contacts something happened to someone ("job_paid", "quote_sent"), so any email series
424
+ * waiting for that event starts or stops for them. The person is found by email, or added; it
425
+ * never signs anyone up for email or changes their consent. Sending the same `key` twice records
426
+ * the event once (`duplicate: true`), so pass your own id for it. `record` names the quote, job or
427
+ * booking it is about. An event more than two days old is kept as a fact a series reads, never
428
+ * a start. Needs the account's token with crm:write (Support Plus and above), never an app key.
429
+ * The answer is the same whether or not the address belongs to someone erased from Contacts.
430
+ *
431
+ * @example
432
+ * await db.recordEvent({ event: 'job_paid', email: 'sam@example.com', record: 'job-1042', key: 'xero:INV-1042' });
433
+ */
434
+ async recordEvent(event) {
435
+ const r = await this.request('POST', '/api/my-crm/v1/events', {
436
+ event: event.event, email: event.email, record: event.record, occurred_at: iso(event.occurredAt), key: event.key,
437
+ first_name: event.firstName, last_name: event.lastName,
438
+ }, false);
439
+ return { duplicate: !!r.duplicate };
440
+ }
283
441
  /** The kinds this token can read, their columns, operators and examples. */
284
442
  schema() {
285
443
  return this.request('GET', '/api/my-crm/v1/rows/schema');
@@ -317,8 +475,13 @@ export class AwesomateClient {
317
475
  after = page.next ?? undefined;
318
476
  } while (after);
319
477
  }
320
- /** One row by id, or null when there is none this token can read. */
478
+ /**
479
+ * One row by id, or null when there is none this token can read. Ids are UUIDs: anything else
480
+ * names no row, so it answers null without asking the hub.
481
+ */
321
482
  async get(kind, id, options = {}) {
483
+ if (!RECORD_ID.test(String(id)))
484
+ return null;
322
485
  try {
323
486
  const qs = options.tz ? `?tz=${encodeURIComponent(options.tz)}` : '';
324
487
  const r = await this.request('GET', `/api/my-crm/v1/rows/${encodeURIComponent(kind)}/${encodeURIComponent(String(id))}${qs}`);
@@ -431,6 +594,29 @@ export class AwesomateClient {
431
594
  async businessMapV1() {
432
595
  return (await this.request('GET', '/api/my-business/v1/map?format=json')).map;
433
596
  }
597
+ /**
598
+ * The business's long documents that exist (brand guide, voice guide, brand from the website,
599
+ * business summary): kind, version, length and where each came from, without the text. Needs
600
+ * the account's token (hosting:read, every plan), never an app key.
601
+ */
602
+ async businessDocuments() {
603
+ return (await this.request('GET', '/api/my-business/v1/documents')).documents;
604
+ }
605
+ /**
606
+ * One of the business's documents with its text (the current version), or null when the
607
+ * business has none of that kind yet. The text is the owner's own: use it for their business
608
+ * (agent instructions, site copy in their voice), never show it to anyone else.
609
+ */
610
+ async businessDocument(kind) {
611
+ try {
612
+ return await this.request('GET', `/api/my-business/v1/documents/${encodeURIComponent(kind)}`);
613
+ }
614
+ catch (err) {
615
+ if (err instanceof AwesomateError && err.code === 'not_found')
616
+ return null;
617
+ throw err;
618
+ }
619
+ }
434
620
  /** Details waiting for the owner's yes on Your business: from the website, the account, a team member or a file. */
435
621
  async businessSuggestions() {
436
622
  return (await this.request('GET', '/api/my-business/v1/proposals')).proposals;
@@ -726,8 +912,13 @@ export class AwesomateAppClient {
726
912
  after = page.next ?? undefined;
727
913
  } while (after);
728
914
  }
729
- /** One record by id, or null when there is none this user may read. */
915
+ /**
916
+ * One record by id, or null when there is none this user may read. Ids are UUIDs: anything
917
+ * else names no record, so it answers null without asking the hub.
918
+ */
730
919
  async get(kind, id) {
920
+ if (!RECORD_ID.test(String(id)))
921
+ return null;
731
922
  try {
732
923
  return (await this.data('GET', `/rows/${encodeURIComponent(kind)}/${encodeURIComponent(String(id))}`)).row;
733
924
  }
@@ -1238,6 +1429,15 @@ export function createAppClient(options) {
1238
1429
  export function createClient(options) {
1239
1430
  return new AwesomateClient(options);
1240
1431
  }
1432
+ function staffBooking(b) {
1433
+ return {
1434
+ bookingId: b.booking_id, status: b.status, service: b.service,
1435
+ serviceName: b.service_name ?? null, calendar: b.calendar, calendarName: b.calendar_name ?? null,
1436
+ startsAt: b.starts_at, endsAt: b.ends_at, seats: b.seats, source: b.source ?? null,
1437
+ answers: b.answers ?? null, contactId: b.contact_id,
1438
+ customer: b.customer ?? null, createdAt: b.created_at, cancelledAt: b.cancelled_at ?? null,
1439
+ };
1440
+ }
1241
1441
  /** The token in a booking's manage link (`/booking?t=...`), or null. */
1242
1442
  export function manageTokenFrom(url) {
1243
1443
  try {
@@ -1312,8 +1512,13 @@ export class AwesomateBookingsClient {
1312
1512
  }
1313
1513
  /**
1314
1514
  * Book a time for a visitor. They get an email with an invite and a link to change or cancel;
1315
- * the business gets a notice. A time taken since you listed it is refused with code `conflict`
1316
- * and `field` saying why (not_open, slot_taken, session_full, day_full): list the times again.
1515
+ * the business gets a notice. A refusal has code `conflict` and `field` saying why:
1516
+ * - `not_open`, `slot_taken`, `too_many_seats`, `session_full`, `day_full`: the time is not
1517
+ * free (taken since you listed it, or not enough places left): list the times again;
1518
+ * - `too_many_open`: this email already has three bookings coming up;
1519
+ * - `monthly_limit`: the business has taken its plan's online bookings for the month.
1520
+ *
1521
+ * Show `personMessage` to the visitor for the last two.
1317
1522
  */
1318
1523
  async book(request) {
1319
1524
  const r = await this.request('POST', '/book', {
@@ -1322,17 +1527,18 @@ export class AwesomateBookingsClient {
1322
1527
  });
1323
1528
  return { bookingId: r.booking_id, created: r.created, startsAt: r.starts_at, endsAt: r.ends_at, manageUrl: r.manage_url };
1324
1529
  }
1325
- /** One booking, from the token in its manage link (manageTokenFrom()). */
1530
+ /** One booking, from the token in its manage link (manageTokenFrom()), with how the business looks so the page can match it. */
1326
1531
  async booking(manageToken) {
1327
1532
  const r = await this.request('GET', `/manage?t=${encodeURIComponent(manageToken)}`, undefined, false);
1328
1533
  const b = r.booking;
1329
1534
  return {
1330
1535
  business: r.business,
1536
+ look: { website: r.look?.website ?? null, logo: r.look?.logo ?? null, colour: r.look?.colour ?? null },
1331
1537
  booking: {
1332
1538
  bookingId: b.booking_id, status: b.status, service: b.service ?? null,
1333
1539
  with: b.with ?? null, startsAt: b.starts_at, endsAt: b.ends_at, timezone: b.timezone,
1334
1540
  location: b.location ?? '', seats: b.seats, canCancel: !!b.can_cancel, canMove: !!b.can_move,
1335
- changesCloseAt: b.changes_close_at,
1541
+ changesCloseAt: b.changes_close_at, priceText: b.price_text ?? '',
1336
1542
  },
1337
1543
  };
1338
1544
  }
@@ -1347,11 +1553,18 @@ export class AwesomateBookingsClient {
1347
1553
  q.set('to', to);
1348
1554
  return (await this.request('GET', `/manage/open-times?${q}`, undefined, false)).times;
1349
1555
  }
1350
- /** Cancel a booking from its manage link. A second cancel answers cancelled: false. */
1556
+ /**
1557
+ * Cancel a booking from its manage link. A second cancel answers cancelled: false. Once changes
1558
+ * have closed (`canCancel` false) it is refused with code `conflict`, field `too_late`.
1559
+ */
1351
1560
  cancel(manageToken, reason) {
1352
1561
  return this.request('POST', '/manage/cancel', { t: manageToken, reason }, false);
1353
1562
  }
1354
- /** Move a booking to a time from openTimesToMove(). */
1563
+ /**
1564
+ * Move a booking to a time from openTimesToMove(). Refused with code `conflict` and field
1565
+ * `too_late` once changes have closed, or `not_open` (or another of book()'s reasons) when the
1566
+ * time is no longer free.
1567
+ */
1355
1568
  async move(manageToken, startsAt) {
1356
1569
  const r = await this.request('POST', '/manage/move', { t: manageToken, starts_at: startsAt }, false);
1357
1570
  return { startsAt: r.starts_at, endsAt: r.ends_at };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/sdk",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Your own Awesomate data from Node and the browser: query contacts and app data with generated types, and sign your app's own users in",
5
5
  "license": "MIT",
6
6
  "type": "module",