@awesomate/sdk 0.23.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +626 -53
  2. package/dist/index.js +247 -13
  3. package/package.json +1 -1
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.23.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>;
@@ -516,12 +702,39 @@ export declare class AwesomateClient {
516
702
  */
517
703
  business(): Promise<BusinessIdentity>;
518
704
  /**
519
- * The business map: the seven divisions every business has, the jobs in each and who holds them,
520
- * which agents and automations help which job and how far each may go, and what is missing, most
521
- * important first. Read only: the owner changes the map in the hub. Needs the account's token
522
- * (hosting:read, every plan), never an app key, and the account must have the business map.
705
+ * The business map: the seven departments every business has, their sub-departments, the roles in
706
+ * each and who holds them, which agents and automations help which role and how far each may go,
707
+ * and what is missing, most important first. Read only: the owner changes the map in the hub.
708
+ * Needs the account's token (hosting:read, every plan), never an app key, and the account must
709
+ * have the business map. Since 0.24.0 in the words the owner sees; businessMapV1() is the old shape.
523
710
  */
524
711
  businessMap(): Promise<BusinessMap>;
712
+ /** Every role on the map, one line each: slug, title, department and who holds it. */
713
+ businessMapRoles(): Promise<BusinessMapRoleSummary[]>;
714
+ /**
715
+ * One role by its slug, with the line each helper's instructions carry (`helpers[].instructionLine`):
716
+ * which role it helps, for whom, how far it may go and where its procedures are.
717
+ */
718
+ businessMapRole(slug: string): Promise<BusinessMapRoleDetail>;
719
+ /**
720
+ * The business map in its first shape, where a department is a `division`, a sub-department a
721
+ * `department` and a role a `job`.
722
+ * @deprecated Use businessMap(), which uses the words the owner sees. v1 is removed once it has
723
+ * gone 30 days unused, and not before 2026-11-06.
724
+ */
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>;
525
738
  /** Details waiting for the owner's yes on Your business: from the website, the account, a team member or a file. */
526
739
  businessSuggestions(): Promise<BusinessSuggestion[]>;
527
740
  /** Grouped numbers (counts, sums) in the Business Data API's shape. */
@@ -592,20 +805,20 @@ export interface BusinessMapHelper {
592
805
  ref: string;
593
806
  label: string;
594
807
  }
595
- /** A job on the map. `slug` is its stable name for routing work: "whoever holds quotes". */
596
- export interface BusinessMapJob {
808
+ /** A role on the map. `slug` is its stable name for routing work: "whoever holds quotes". */
809
+ export interface BusinessMapRole {
597
810
  id: number;
598
811
  slug: string;
599
812
  title: string;
600
813
  mission: string | null;
601
814
  /** active: someone holds it. open: nobody does. hire_next: the owner's next hire. */
602
815
  state: 'active' | 'open' | 'hire_next';
603
- isOwnerJob: boolean;
604
- /** Runs its division: the division manager, above its departments, so departmentNo is null (the owner's job excepted). */
605
- headsDivision: boolean;
606
- /** Runs its department: the person responsible for it, and for its department-level KPIs. */
816
+ isOwnerRole: boolean;
817
+ /** Runs its department: above its sub-departments, so subDepartmentNo is null (the owner's role excepted). */
607
818
  headsDepartment: boolean;
608
- departmentNo: number | null;
819
+ /** Runs its sub-department: the person responsible for it, and for its KPIs. */
820
+ headsSubDepartment: boolean;
821
+ subDepartmentNo: number | null;
609
822
  reportsTo: {
610
823
  id: number;
611
824
  title: string;
@@ -628,12 +841,12 @@ export interface BusinessMapJob {
628
841
  exceptions: string | null;
629
842
  minutesSavedPerRun: number | null;
630
843
  }>;
631
- /** The tag this job's procedures carry in 1Brain, such as 'job:quotes'. The map keeps no procedure text. */
844
+ /** The tag this role's procedures carry in 1Brain, such as 'job:quotes'. The map keeps no procedure text. */
632
845
  proceduresTag: string;
633
- /** Quarterly priorities on this hat, every quarter. */
846
+ /** Quarterly priorities on this role, every quarter. */
634
847
  priorities: BusinessMapPriority[];
635
848
  }
636
- /** A quarterly priority on a hat: one of the few things it must move this quarter. */
849
+ /** A quarterly priority on a role: one of the few things it must move this quarter. */
637
850
  export interface BusinessMapPriority {
638
851
  id: number;
639
852
  /** '2026-Q4'. */
@@ -649,15 +862,15 @@ export interface BusinessMapPriority {
649
862
  export interface BusinessMapPathStep {
650
863
  position: number;
651
864
  label: string;
652
- divisionNo: 1 | 2 | 3 | 4 | 5 | 6 | 7;
865
+ departmentNo: 1 | 2 | 3 | 4 | 5 | 6 | 7;
653
866
  verb: string;
654
- /** The job that looks after the step, when the owner named one. */
655
- job: {
867
+ /** The role that looks after the step, when the owner named one. */
868
+ role: {
656
869
  id: number;
657
870
  slug: string;
658
871
  title: string;
659
872
  } | null;
660
- /** Who looks after it: the job's holder, else whoever runs the division (byDefault). */
873
+ /** Who looks after it: the role's holder, else whoever runs the department (byDefault). */
661
874
  owner: {
662
875
  name: string;
663
876
  byDefault: boolean;
@@ -665,8 +878,23 @@ export interface BusinessMapPathStep {
665
878
  /** Where this step hands over to the next, in the owner's words. */
666
879
  handoffRule: string | null;
667
880
  }
668
- /** One of the seven divisions, in board order (7 first). */
669
- export interface BusinessMapDivision {
881
+ /** A sub-department and who runs it: the holder of the role that heads it, else whoever runs the department (byDefault). */
882
+ export interface BusinessMapSubDepartment {
883
+ no: number;
884
+ name: string;
885
+ gloss: string;
886
+ runBy: {
887
+ name: string;
888
+ byDefault: boolean;
889
+ };
890
+ headRole: {
891
+ id: number;
892
+ slug: string;
893
+ title: string;
894
+ } | null;
895
+ }
896
+ /** One of the seven departments, in board order (7 first). */
897
+ export interface BusinessMapDepartment {
670
898
  no: 1 | 2 | 3 | 4 | 5 | 6 | 7;
671
899
  /** Envision, Form, Promise, Balance, Fulfil, Refine, Share. */
672
900
  verb: string;
@@ -678,12 +906,230 @@ export interface BusinessMapDivision {
678
906
  name: string;
679
907
  byDefault: boolean;
680
908
  };
909
+ roles: BusinessMapRole[];
910
+ /** Helping here but not yet put on a role, with how they were placed, in words. */
911
+ helpers: Array<BusinessMapHelper & {
912
+ placedBy: string;
913
+ }>;
914
+ subDepartments: BusinessMapSubDepartment[];
915
+ ideas: Array<{
916
+ label: string;
917
+ what: string;
918
+ status: 'live' | 'library' | 'coming' | 'building' | 'planned';
919
+ path?: string;
920
+ }>;
921
+ numbers: Array<{
922
+ label: string;
923
+ kind: 'lead' | 'result';
924
+ }>;
925
+ question: string;
926
+ /** 1Brain Departments whose procedures belong in this department. */
927
+ oneBrainDepartments: string[];
928
+ /** People on the map who hold no role yet. They sit in Form (department 1) for display; empty for every other department. */
929
+ noRoleYet: Array<{
930
+ id: number;
931
+ name: string;
932
+ }>;
933
+ }
934
+ /** The business map, as businessMap() returns it. No email addresses. */
935
+ export interface BusinessMap {
936
+ version: 2;
937
+ business: {
938
+ name: string | null;
939
+ ownerName: string;
940
+ };
941
+ /** false: the owner has not started the map, so it is worked out from what the account runs. */
942
+ stored: boolean;
943
+ /** access: what the person may do in the hub (owner, full or view). */
944
+ people: Array<{
945
+ id: number | null;
946
+ name: string;
947
+ kind: 'owner' | 'staff' | 'contractor' | 'adviser' | null;
948
+ access: 'owner' | 'full' | 'view' | null;
949
+ location: string | null;
950
+ fromTeamAccess: boolean;
951
+ }>;
952
+ departments: BusinessMapDepartment[];
953
+ /** Helpers we could not place on a department. */
954
+ unplaced: Array<BusinessMapHelper & {
955
+ placedBy: string;
956
+ }>;
957
+ /** How a customer moves through the business. stored false: suggested for its kind of business, not set by the owner yet. */
958
+ path: {
959
+ stored: boolean;
960
+ template: {
961
+ key: string;
962
+ label: string;
963
+ } | null;
964
+ steps: BusinessMapPathStep[];
965
+ };
966
+ /** This quarter, '2026-Q4' (UTC): the one the map shows priorities for. */
967
+ quarter: string;
968
+ /** What is missing, most important first. */
969
+ gaps: Array<{
970
+ kind: 'procedure_first' | 'no_helper' | 'role_open' | 'path_missing' | 'path_unowned' | 'owner_everywhere' | 'unplaced';
971
+ department: number | null;
972
+ title: string;
973
+ detail: string;
974
+ action: {
975
+ label: string;
976
+ path: string;
977
+ } | null;
978
+ }>;
979
+ /** oneBrainCategory: the 1Brain category this business is, once linked. */
980
+ settings: {
981
+ adviser: string | null;
982
+ runsWeek: string | null;
983
+ oneBrainCategory: string | null;
984
+ };
985
+ counts: {
986
+ people: number;
987
+ helpers: number;
988
+ placed: number;
989
+ departmentsWithHelpers: number;
990
+ roles: number;
991
+ };
992
+ /** Sources that could not be read just now: a missing helper may simply not have been read. */
993
+ unavailable: string[];
994
+ }
995
+ /** One role in businessMapRoles(). */
996
+ export interface BusinessMapRoleSummary {
997
+ slug: string;
998
+ title: string;
999
+ state: BusinessMapRole['state'];
1000
+ department: {
1001
+ no: number;
1002
+ verb: string;
1003
+ name: string;
1004
+ };
1005
+ /** The accountable holder, else the first; null when nobody holds it. */
1006
+ holder: string | null;
1007
+ }
1008
+ /** One role, as businessMapRole() returns it. */
1009
+ export interface BusinessMapRoleDetail {
1010
+ slug: string;
1011
+ title: string;
1012
+ mission: string | null;
1013
+ state: BusinessMapRole['state'];
1014
+ department: {
1015
+ no: number;
1016
+ verb: string;
1017
+ name: string;
1018
+ };
1019
+ headsDepartment: boolean;
1020
+ headsSubDepartment: boolean;
1021
+ reportsTo: {
1022
+ title: string;
1023
+ slug: string | null;
1024
+ } | null;
1025
+ holder: string | null;
1026
+ procedures: {
1027
+ where: '1Brain';
1028
+ tag: string;
1029
+ };
1030
+ /** This quarter's and next quarter's. */
1031
+ priorities: Array<{
1032
+ quarter: string;
1033
+ title: string;
1034
+ status: BusinessMapPriority['status'];
1035
+ owner: string | null;
1036
+ dueOn: string | null;
1037
+ }>;
1038
+ pathSteps: Array<{
1039
+ position: number;
1040
+ label: string;
1041
+ handoffRule: string | null;
1042
+ }>;
1043
+ holders: Array<{
1044
+ name: string;
1045
+ accountable: boolean;
1046
+ timeSharePct: number | null;
1047
+ }>;
1048
+ responsibilities: Array<{
1049
+ text: string;
1050
+ level: BusinessMapLevel;
1051
+ levelLabel: string;
1052
+ exceptions: string | null;
1053
+ }>;
1054
+ helpers: Array<{
1055
+ kind: BusinessMapHelper['kind'];
1056
+ ref: string;
1057
+ label: string;
1058
+ supervisor: string;
1059
+ level: BusinessMapLevel | null;
1060
+ levelLabel: string | null;
1061
+ exceptions: string | null;
1062
+ instructionLine: string;
1063
+ }>;
1064
+ }
1065
+ /**
1066
+ * A job on the v1 map (a role).
1067
+ * @deprecated v1 shape, from businessMapV1(). Use BusinessMapRole.
1068
+ */
1069
+ export interface BusinessMapJob {
1070
+ id: number;
1071
+ slug: string;
1072
+ title: string;
1073
+ mission: string | null;
1074
+ state: 'active' | 'open' | 'hire_next';
1075
+ isOwnerJob: boolean;
1076
+ /** Runs its division (a department). */
1077
+ headsDivision: boolean;
1078
+ /** Runs its department (a sub-department). */
1079
+ headsDepartment: boolean;
1080
+ /** The sub-department. */
1081
+ departmentNo: number | null;
1082
+ reportsTo: {
1083
+ id: number;
1084
+ title: string;
1085
+ } | null;
1086
+ holders: BusinessMapRole['holders'];
1087
+ responsibilities: BusinessMapRole['responsibilities'];
1088
+ helpers: BusinessMapRole['helpers'];
1089
+ proceduresTag: string;
1090
+ priorities: BusinessMapPriority[];
1091
+ }
1092
+ /**
1093
+ * One step of the customer's path on the v1 map.
1094
+ * @deprecated v1 shape, from businessMapV1(). Use BusinessMapPathStep.
1095
+ */
1096
+ export interface BusinessMapPathStepV1 {
1097
+ position: number;
1098
+ label: string;
1099
+ /** The department. */
1100
+ divisionNo: 1 | 2 | 3 | 4 | 5 | 6 | 7;
1101
+ verb: string;
1102
+ /** The role. */
1103
+ job: {
1104
+ id: number;
1105
+ slug: string;
1106
+ title: string;
1107
+ } | null;
1108
+ owner: {
1109
+ name: string;
1110
+ byDefault: boolean;
1111
+ };
1112
+ handoffRule: string | null;
1113
+ }
1114
+ /**
1115
+ * A division on the v1 map (a department).
1116
+ * @deprecated v1 shape, from businessMapV1(). Use BusinessMapDepartment.
1117
+ */
1118
+ export interface BusinessMapDivision {
1119
+ no: 1 | 2 | 3 | 4 | 5 | 6 | 7;
1120
+ verb: string;
1121
+ name: string;
1122
+ purpose: string;
1123
+ stage: 'survive' | 'grow' | 'scale';
1124
+ runBy: {
1125
+ name: string;
1126
+ byDefault: boolean;
1127
+ };
681
1128
  jobs: BusinessMapJob[];
682
- /** Helping here but not yet put on a job, with how they were placed, in words. */
683
1129
  helpers: Array<BusinessMapHelper & {
684
1130
  placedBy: string;
685
1131
  }>;
686
- /** Each department and who runs it: the holder of the job that heads it, else whoever runs the division (byDefault). */
1132
+ /** The sub-departments. */
687
1133
  departments: Array<{
688
1134
  no: number;
689
1135
  name: string;
@@ -698,33 +1144,26 @@ export interface BusinessMapDivision {
698
1144
  title: string;
699
1145
  } | null;
700
1146
  }>;
701
- ideas: Array<{
702
- label: string;
703
- what: string;
704
- status: 'live' | 'library' | 'coming' | 'building' | 'planned';
705
- path?: string;
706
- }>;
707
- numbers: Array<{
708
- label: string;
709
- kind: 'lead' | 'result';
710
- }>;
1147
+ ideas: BusinessMapDepartment['ideas'];
1148
+ numbers: BusinessMapDepartment['numbers'];
711
1149
  question: string;
712
- /** 1Brain departments whose procedures belong in this division. */
713
1150
  oneBrainDepartments: string[];
714
- /** People on the map who wear no hat yet. They sit in Form (division 1) for display; empty for every other division. */
715
1151
  noHatYet: Array<{
716
1152
  id: number;
717
1153
  name: string;
718
1154
  }>;
719
1155
  }
720
- /** The business map, as businessMap() returns it. No email addresses. */
721
- export interface BusinessMap {
1156
+ /**
1157
+ * The business map in its first shape, as businessMapV1() returns it.
1158
+ * @deprecated Use BusinessMap, from businessMap().
1159
+ */
1160
+ export interface BusinessMapV1 {
722
1161
  business: {
723
1162
  name: string | null;
724
1163
  ownerName: string;
725
1164
  };
726
- /** false: the owner has not started the map, so it is worked out from what the account runs. */
727
1165
  stored: boolean;
1166
+ /** role: the person's access. */
728
1167
  people: Array<{
729
1168
  id: number | null;
730
1169
  name: string;
@@ -734,22 +1173,18 @@ export interface BusinessMap {
734
1173
  fromTeamAccess: boolean;
735
1174
  }>;
736
1175
  divisions: BusinessMapDivision[];
737
- /** Helpers we could not place on a division. */
738
1176
  unplaced: Array<BusinessMapHelper & {
739
1177
  placedBy: string;
740
1178
  }>;
741
- /** How a customer moves through the business. stored false: suggested for its kind of business, not set by the owner yet. */
742
1179
  path: {
743
1180
  stored: boolean;
744
1181
  template: {
745
1182
  key: string;
746
1183
  label: string;
747
1184
  } | null;
748
- steps: BusinessMapPathStep[];
1185
+ steps: BusinessMapPathStepV1[];
749
1186
  };
750
- /** This quarter, '2026-Q4' (UTC): the one the map shows priorities for. */
751
1187
  quarter: string;
752
- /** What is missing, most important first. */
753
1188
  gaps: Array<{
754
1189
  kind: 'procedure_first' | 'no_helper' | 'job_open' | 'path_missing' | 'path_unowned' | 'owner_everywhere' | 'unplaced';
755
1190
  division: number | null;
@@ -763,6 +1198,7 @@ export interface BusinessMap {
763
1198
  settings: {
764
1199
  adviser: string | null;
765
1200
  runsWeek: string | null;
1201
+ oneBrainCategory: string | null;
766
1202
  };
767
1203
  counts: {
768
1204
  people: number;
@@ -771,7 +1207,6 @@ export interface BusinessMap {
771
1207
  divisionsWithHelpers: number;
772
1208
  jobs: number;
773
1209
  };
774
- /** Sources that could not be read just now: a missing helper may simply not have been read. */
775
1210
  unavailable: string[];
776
1211
  }
777
1212
  /** A detail waiting for the owner's yes. */
@@ -784,6 +1219,44 @@ export interface BusinessSuggestion {
784
1219
  sourceRef: string | null;
785
1220
  recordedAt: string;
786
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
+ }
787
1260
  /** One attribute (column) of a kind. */
788
1261
  export interface AttributeSpec {
789
1262
  key: string;
@@ -794,6 +1267,20 @@ export interface AttributeSpec {
794
1267
  sensitivity?: 'ordinary' | 'personal' | 'sensitive';
795
1268
  /** Default false. Only readable attributes reach query(), get() and the generated types. */
796
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>;
797
1284
  }
798
1285
  /** A kind to define: a table an app keeps. */
799
1286
  export interface KindSpec {
@@ -809,16 +1296,22 @@ export interface KindSpec {
809
1296
  to_label?: string;
810
1297
  required?: boolean;
811
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;
812
1301
  }
813
1302
  /** A kind as the account has defined it. */
814
- export interface KindDescription extends Omit<KindSpec, 'attributes'> {
1303
+ export interface KindDescription extends Omit<KindSpec, 'attributes' | 'access'> {
815
1304
  storage: 'plain' | 'tracked';
816
1305
  attributes: Array<Required<Pick<AttributeSpec, 'key' | 'label' | 'type' | 'required' | 'sensitivity' | 'readable_by_ai'>> & {
817
1306
  choices: string[] | null;
1307
+ visible_to: string[] | null;
818
1308
  }>;
1309
+ /** The kind's access rules. A kind with none set reads owner and staff `all`, for reading and writing. */
1310
+ access: KindAccess;
819
1311
  views: {
820
1312
  all: string;
821
1313
  for_agents: string;
1314
+ for_app: string;
822
1315
  };
823
1316
  }
824
1317
  /** Options for write(). */
@@ -893,6 +1386,8 @@ export interface RecipeSpec {
893
1386
  export interface RecipeDescription extends Required<Omit<RecipeSpec, 'description' | 'params'>> {
894
1387
  description: string | null;
895
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[];
896
1391
  updated_at: string;
897
1392
  }
898
1393
  /** What a recipe run wrote. */
@@ -1099,7 +1594,10 @@ export declare class AwesomateAppClient {
1099
1594
  queryAll<K extends KindName, S extends keyof RowOf<K> = keyof RowOf<K>>(kind: K, options?: Omit<QueryOptions<RowOf<K>, S>, 'after'> & {
1100
1595
  maxRows?: number;
1101
1596
  }): AsyncGenerator<Pick<RowOf<K>, S>>;
1102
- /** 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
+ */
1103
1601
  get<K extends KindName>(kind: K, id: string): Promise<RowOf<K> | null>;
1104
1602
  /** Create a record (returns its id) or change one with options.id, inside the kind's write rule for this user. */
1105
1603
  write<K extends KindName>(kind: K, data: Partial<RowOf<K>> | Record<string, unknown>, options?: WriteOptions): Promise<string>;
@@ -1237,6 +1735,8 @@ export interface BookableService {
1237
1735
  /** The price as the business wrote it. Shown, never charged. */
1238
1736
  price_text: string;
1239
1737
  location: string;
1738
+ /** How many hours before the start a customer can still cancel or move online. */
1739
+ cancel_cutoff_hours: number;
1240
1740
  intake: BookingQuestion[];
1241
1741
  calendars: Array<{
1242
1742
  key: string;
@@ -1301,6 +1801,66 @@ export interface ManagedBooking {
1301
1801
  canCancel: boolean;
1302
1802
  canMove: boolean;
1303
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;
1304
1864
  }
1305
1865
  /** Options for createBookingsClient(). */
1306
1866
  export interface BookingsClientOptions {
@@ -1342,13 +1902,19 @@ export declare class AwesomateBookingsClient {
1342
1902
  }): Promise<OpenTime[]>;
1343
1903
  /**
1344
1904
  * Book a time for a visitor. They get an email with an invite and a link to change or cancel;
1345
- * the business gets a notice. A time taken since you listed it is refused with code `conflict`
1346
- * 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.
1347
1912
  */
1348
1913
  book(request: BookingRequest): Promise<BookingResult>;
1349
- /** 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. */
1350
1915
  booking(manageToken: string): Promise<{
1351
1916
  business: string;
1917
+ look: BookingPageLook;
1352
1918
  booking: ManagedBooking;
1353
1919
  }>;
1354
1920
  /** The times a booking could move to, leaving its own time out. */
@@ -1356,11 +1922,18 @@ export declare class AwesomateBookingsClient {
1356
1922
  from?: Date | string;
1357
1923
  to?: Date | string;
1358
1924
  }): Promise<OpenTime[]>;
1359
- /** 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
+ */
1360
1929
  cancel(manageToken: string, reason?: string): Promise<{
1361
1930
  cancelled: boolean;
1362
1931
  }>;
1363
- /** 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
+ */
1364
1937
  move(manageToken: string, startsAt: string): Promise<{
1365
1938
  startsAt: string;
1366
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.23.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}`);
@@ -402,14 +565,58 @@ export class AwesomateClient {
402
565
  return this.request('GET', '/api/my-business/v1/identity?format=json');
403
566
  }
404
567
  /**
405
- * The business map: the seven divisions every business has, the jobs in each and who holds them,
406
- * which agents and automations help which job and how far each may go, and what is missing, most
407
- * important first. Read only: the owner changes the map in the hub. Needs the account's token
408
- * (hosting:read, every plan), never an app key, and the account must have the business map.
568
+ * The business map: the seven departments every business has, their sub-departments, the roles in
569
+ * each and who holds them, which agents and automations help which role and how far each may go,
570
+ * and what is missing, most important first. Read only: the owner changes the map in the hub.
571
+ * Needs the account's token (hosting:read, every plan), never an app key, and the account must
572
+ * have the business map. Since 0.24.0 in the words the owner sees; businessMapV1() is the old shape.
409
573
  */
410
574
  async businessMap() {
575
+ return (await this.request('GET', '/api/my-business/v2/map?format=json')).map;
576
+ }
577
+ /** Every role on the map, one line each: slug, title, department and who holds it. */
578
+ async businessMapRoles() {
579
+ return (await this.request('GET', '/api/my-business/v2/map/roles')).roles;
580
+ }
581
+ /**
582
+ * One role by its slug, with the line each helper's instructions carry (`helpers[].instructionLine`):
583
+ * which role it helps, for whom, how far it may go and where its procedures are.
584
+ */
585
+ async businessMapRole(slug) {
586
+ return (await this.request('GET', `/api/my-business/v2/map/roles/${encodeURIComponent(slug)}`)).role;
587
+ }
588
+ /**
589
+ * The business map in its first shape, where a department is a `division`, a sub-department a
590
+ * `department` and a role a `job`.
591
+ * @deprecated Use businessMap(), which uses the words the owner sees. v1 is removed once it has
592
+ * gone 30 days unused, and not before 2026-11-06.
593
+ */
594
+ async businessMapV1() {
411
595
  return (await this.request('GET', '/api/my-business/v1/map?format=json')).map;
412
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
+ }
413
620
  /** Details waiting for the owner's yes on Your business: from the website, the account, a team member or a file. */
414
621
  async businessSuggestions() {
415
622
  return (await this.request('GET', '/api/my-business/v1/proposals')).proposals;
@@ -705,8 +912,13 @@ export class AwesomateAppClient {
705
912
  after = page.next ?? undefined;
706
913
  } while (after);
707
914
  }
708
- /** 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
+ */
709
919
  async get(kind, id) {
920
+ if (!RECORD_ID.test(String(id)))
921
+ return null;
710
922
  try {
711
923
  return (await this.data('GET', `/rows/${encodeURIComponent(kind)}/${encodeURIComponent(String(id))}`)).row;
712
924
  }
@@ -1217,6 +1429,15 @@ export function createAppClient(options) {
1217
1429
  export function createClient(options) {
1218
1430
  return new AwesomateClient(options);
1219
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
+ }
1220
1441
  /** The token in a booking's manage link (`/booking?t=...`), or null. */
1221
1442
  export function manageTokenFrom(url) {
1222
1443
  try {
@@ -1291,8 +1512,13 @@ export class AwesomateBookingsClient {
1291
1512
  }
1292
1513
  /**
1293
1514
  * Book a time for a visitor. They get an email with an invite and a link to change or cancel;
1294
- * the business gets a notice. A time taken since you listed it is refused with code `conflict`
1295
- * 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.
1296
1522
  */
1297
1523
  async book(request) {
1298
1524
  const r = await this.request('POST', '/book', {
@@ -1301,17 +1527,18 @@ export class AwesomateBookingsClient {
1301
1527
  });
1302
1528
  return { bookingId: r.booking_id, created: r.created, startsAt: r.starts_at, endsAt: r.ends_at, manageUrl: r.manage_url };
1303
1529
  }
1304
- /** 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. */
1305
1531
  async booking(manageToken) {
1306
1532
  const r = await this.request('GET', `/manage?t=${encodeURIComponent(manageToken)}`, undefined, false);
1307
1533
  const b = r.booking;
1308
1534
  return {
1309
1535
  business: r.business,
1536
+ look: { website: r.look?.website ?? null, logo: r.look?.logo ?? null, colour: r.look?.colour ?? null },
1310
1537
  booking: {
1311
1538
  bookingId: b.booking_id, status: b.status, service: b.service ?? null,
1312
1539
  with: b.with ?? null, startsAt: b.starts_at, endsAt: b.ends_at, timezone: b.timezone,
1313
1540
  location: b.location ?? '', seats: b.seats, canCancel: !!b.can_cancel, canMove: !!b.can_move,
1314
- changesCloseAt: b.changes_close_at,
1541
+ changesCloseAt: b.changes_close_at, priceText: b.price_text ?? '',
1315
1542
  },
1316
1543
  };
1317
1544
  }
@@ -1326,11 +1553,18 @@ export class AwesomateBookingsClient {
1326
1553
  q.set('to', to);
1327
1554
  return (await this.request('GET', `/manage/open-times?${q}`, undefined, false)).times;
1328
1555
  }
1329
- /** 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
+ */
1330
1560
  cancel(manageToken, reason) {
1331
1561
  return this.request('POST', '/manage/cancel', { t: manageToken, reason }, false);
1332
1562
  }
1333
- /** 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
+ */
1334
1568
  async move(manageToken, startsAt) {
1335
1569
  const r = await this.request('POST', '/manage/move', { t: manageToken, starts_at: startsAt }, false);
1336
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.23.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",