@awesomate/sdk 0.34.0 → 0.51.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
@@ -26,7 +26,7 @@
26
26
  * Docs: https://hub.awesomate.ai/docs/sdk/
27
27
  */
28
28
  /** This package's version, sent to the hub with every server call. */
29
- export declare const VERSION = "0.34.0";
29
+ export declare const VERSION = "0.51.0";
30
30
  /** Augmented by the generated awesomate.d.ts, so each kind's rows are typed. */
31
31
  export interface Kinds {
32
32
  }
@@ -135,7 +135,7 @@ export interface QueryOptions<T, S extends keyof T = keyof T> {
135
135
  * read, tags included, any case. Narrows like where does, and the two combine.
136
136
  */
137
137
  search?: string;
138
- /** IANA zone for dates and time variables. Default Australia/Sydney. */
138
+ /** IANA zone for dates and time variables. Default: the business's time zone from Your business, Sydney if none is set. */
139
139
  tz?: string;
140
140
  }
141
141
  /** What the hub assumed for a query (order, limit, time zone, what each time variable meant), so it can be said back. */
@@ -243,6 +243,28 @@ export interface TaskCard {
243
243
  canDrop: boolean;
244
244
  canEdit: boolean;
245
245
  } | null;
246
+ /**
247
+ * Set for a task given to a role on the business map rather than a person (an agent's or a
248
+ * stage's): everyone in the role sees it until one of them takes it. `queued` while nobody has;
249
+ * `takenBy` once someone has; `mayTake` when this token's owner may take it with take(). Null or
250
+ * absent for every other card.
251
+ */
252
+ role?: {
253
+ jobId: number;
254
+ slug: string | null;
255
+ title: string | null;
256
+ queued: boolean;
257
+ takenBy: TaskPerson | null;
258
+ /** Nobody took it in 4 business hours, so it went to the person responsible for the role. */
259
+ unclaimed?: boolean;
260
+ /** While it waits: when it goes to the person responsible if nobody takes it, and that as a sentence. */
261
+ goesTo?: {
262
+ at: string;
263
+ words: string;
264
+ } | null;
265
+ inRole: boolean;
266
+ mayTake: boolean;
267
+ } | null;
246
268
  }
247
269
  /** A card's one-tap action, by name (the hub page behind the card runs it). */
248
270
  export type TaskCardAction = {
@@ -258,6 +280,11 @@ export type TaskCardAction = {
258
280
  } | {
259
281
  name: 'finish_task';
260
282
  taskId: number;
283
+ }
284
+ /** A role's task nobody has taken yet: take() makes it yours. */
285
+ | {
286
+ name: 'take_task';
287
+ taskId: number;
261
288
  } | {
262
289
  name: 'approve_public_reply';
263
290
  taskId: number;
@@ -337,6 +364,16 @@ export interface DecisionCard {
337
364
  kind: 'person' | 'owners';
338
365
  label: string;
339
366
  };
367
+ /**
368
+ * A role's decision within the role's limits: anyone in `role` who can decide it may answer it, and
369
+ * the first answer decides it. `people` is how many may answer it now; `line` is the sentence the
370
+ * hub shows ("Any of you in Bookkeeping can answer."). Null or absent when only `decider` can.
371
+ */
372
+ shared?: {
373
+ role: string;
374
+ people: number;
375
+ line: string;
376
+ } | null;
340
377
  /** Work waits behind it: a yes lets it carry on. */
341
378
  holdsWork: boolean;
342
379
  /** What may be pressed now. Always empty for an account token: deciding is a person's tap in the hub. */
@@ -459,15 +496,49 @@ export interface TaskRecord {
459
496
  handoffs: number;
460
497
  };
461
498
  }
462
- /** A task to give: to someone on the account by email, or to the owner when `forEmail` is left out. */
499
+ /**
500
+ * A task to give: to someone on the account by email, to a role on the business map by its key, or
501
+ * to the owner when both are left out. Send `forEmail` or `role`, never both.
502
+ */
463
503
  export interface GiveTask {
464
504
  title: string;
465
505
  detail?: string;
466
506
  forEmail?: string;
507
+ /**
508
+ * A role on the business map, by its key (`slug` in `businessMap()`'s roles), instead of a person.
509
+ * Everyone in the role sees it until one of them takes it; a role with nobody in it who can act
510
+ * sends it to the owner. A role not on the map is refused (`serverCode: 'unknown_role'`, with the
511
+ * map's roles in the error's `roles`). Needs the business map (`business_map`).
512
+ */
513
+ role?: string;
467
514
  /** ISO time it is due. */
468
515
  dueAt?: string;
469
516
  /** A hub path the task points at, e.g. /contacts/people/... */
470
517
  linkPath?: string;
518
+ /**
519
+ * Something that can't wait: a hot lead, a fault, a staff alert. It goes to the person's linked
520
+ * chats straight away, past their daily limit, and in their quiet hours only to the chat they chose
521
+ * to be woken in (or not until morning, if they chose none). At most five a day per person; urgent
522
+ * wears off a day after it is given. Leave it out for anything else.
523
+ */
524
+ urgent?: boolean;
525
+ }
526
+ /** What `db.tasks.give()` answers. */
527
+ export interface GivenTask {
528
+ key: string;
529
+ id: number;
530
+ /** Who has it now: null only for a role's task still waiting for someone in the role to take it. */
531
+ holder: TaskPerson | null;
532
+ /** Whether the person it went to was emailed (the role's responsible person, for a role's task). */
533
+ emailed: boolean;
534
+ urgent: boolean;
535
+ /** Set when it was given to a role: `queued` while it waits in the role, false when the role had nobody and it went to the owner. */
536
+ role?: {
537
+ slug: string;
538
+ title: string;
539
+ queued: boolean;
540
+ };
541
+ note?: string;
471
542
  }
472
543
  /** A file or folder in the account's Files (the folders on its automation account: public/, private/, temp/). */
473
544
  export interface FileEntry {
@@ -555,6 +626,11 @@ export interface AwesomateErrorDetails {
555
626
  * unavailable when the hub said (serverCode crm_maintenance: the account's Contacts is being moved).
556
627
  */
557
628
  retryAfter?: number;
629
+ /** With unknown_role (giving a task to a role not on the business map): the map's roles, by key and name. */
630
+ roles?: Array<{
631
+ slug: string;
632
+ title: string;
633
+ }>;
558
634
  }
559
635
  /**
560
636
  * Every failure from the hub. `message` is for you, the developer; `personMessage`, when the hub
@@ -592,6 +668,11 @@ export declare class AwesomateError extends Error {
592
668
  * being moved). Wait at least that long, then try once more; retrying at once only meets the same answer.
593
669
  */
594
670
  readonly retryAfter?: number;
671
+ /** With serverCode unknown_role (`tasks.give({ role })` naming a role not on the business map): the map's roles, by key and name. */
672
+ readonly roles?: Array<{
673
+ slug: string;
674
+ title: string;
675
+ }>;
595
676
  constructor(
596
677
  /** Why it failed. */
597
678
  code: ErrorCode, message: string,
@@ -722,6 +803,7 @@ export declare class AwesomateClient {
722
803
  *
723
804
  * @example
724
805
  * await db.tasks.give({ title: 'Call Sam back about the leak', forEmail: 'jo@brightwater.example', dueAt: '2026-10-06T09:00:00+11:00' });
806
+ * await db.tasks.give({ title: 'Hot lead: Sam wants a quote today', forEmail: 'jo@brightwater.example', urgent: true });
725
807
  */
726
808
  readonly tasks: {
727
809
  /** The board: waiting now, put off, with someone else, recently decided. `all` shows everyone's, not only yours. hosting:read. */
@@ -745,15 +827,27 @@ export declare class AwesomateClient {
745
827
  decisionRecord: (taskId: number | string) => Promise<TaskRecord & {
746
828
  kind: "decision";
747
829
  }>;
748
- /** Give a task. The person gets it on their board, and an email when they are not the one giving it. hosting:manage. */
749
- give: (task: GiveTask) => Promise<{
750
- key: string;
751
- id: number;
752
- holder: TaskPerson;
753
- emailed: boolean;
754
- }>;
755
- /** Change a given task: whoever wrote it, or the owner. hosting:manage. */
756
- update: (id: number | string, patch: Partial<Omit<GiveTask, "forEmail">>) => Promise<void>;
830
+ /**
831
+ * Give a task. The person gets it on their board, an email when they are not the one giving it,
832
+ * and a card in any chat they linked, within their quiet hours and daily limit (`urgent` goes
833
+ * straight away). `urgent` in the result says whether it went as urgent: a person's sixth urgent
834
+ * task in a day goes as an ordinary one, and `note` says so. hosting:manage.
835
+ */
836
+ give: (task: GiveTask) => Promise<GivenTask>;
837
+ /**
838
+ * Change a given task: whoever wrote it, or the owner. `urgent: false` takes urgent off; a task can
839
+ * only be marked urgent when it is given. hosting:manage.
840
+ */
841
+ update: (id: number | string, patch: Partial<Omit<GiveTask, "forEmail" | "urgent">> & {
842
+ urgent?: false;
843
+ }) => Promise<void>;
844
+ /**
845
+ * Take a role's task (a card with `role.mayTake`): it becomes the owner's, and everyone else in
846
+ * the role sees who took it. Only someone in the role can take it, so this works for the owner
847
+ * only when they are in that role (serverCode `not_in_role` otherwise). Someone who got there
848
+ * first answers `taken`, with their name in the message. hosting:manage.
849
+ */
850
+ take: (id: number | string) => Promise<void>;
757
851
  /** Mark a given task done: the person who has it, whoever wrote it, or the owner. hosting:manage. */
758
852
  done: (id: number | string) => Promise<void>;
759
853
  /** Take a given task off the board without doing it: whoever wrote it, or the owner. hosting:manage. */
@@ -1022,6 +1116,27 @@ export declare class AwesomateClient {
1022
1116
  addTags: (id: string, tags: string[]) => Promise<ContactTagsResult>;
1023
1117
  /** Take tags off a person, up to 20 a call. Answers how many they had. */
1024
1118
  removeTags: (id: string, tags: string[]) => Promise<ContactTagsResult>;
1119
+ /**
1120
+ * Record that the person asked to stop getting email, by reply, phone, in person or in writing
1121
+ * (`how`), at the time they asked (`askedAt`, default now, at most 30 days back). With no `list`
1122
+ * it stops every email, booking confirmations and other service emails included; with a list's
1123
+ * key, that list only. Recorded as the person's own withdrawal, so a later import never puts
1124
+ * them back. Answers `changed: false` when it was already recorded. The law gives 5 business
1125
+ * days from when they asked. A person with no email address is refused (validation).
1126
+ */
1127
+ unsubscribe: (id: string, options: {
1128
+ how: ContactStopHow;
1129
+ list?: string;
1130
+ askedAt?: Date | string;
1131
+ }) => Promise<ContactUnsubscribeResult>;
1132
+ /**
1133
+ * Prepare an erasure, for when someone asks for their details to be deleted. Answers what
1134
+ * erasing the person would take (`counts`) and `finishInHub`, the hub link where an Owner of the
1135
+ * business finishes it, signed in as themselves, by typing the person's email address. Nothing
1136
+ * is erased here: no token can erase anyone, because an erasure can't be undone. Only an Owner's
1137
+ * token may prepare one (403 `owner_only`). Null when there is no such person.
1138
+ */
1139
+ erasurePreview: (id: string) => Promise<ContactErasurePreview | null>;
1025
1140
  /**
1026
1141
  * Add a contact field. The hub makes its key from the label with a random part and answers it:
1027
1142
  * use that key. Nothing here changes a field that exists. `ai_may_read` decides, once and now,
@@ -1151,6 +1266,18 @@ export declare class AwesomateClient {
1151
1266
  get<K extends KindName>(kind: K, id: string, options?: {
1152
1267
  tz?: string;
1153
1268
  }): Promise<RowOf<K> | null>;
1269
+ /**
1270
+ * What points at one row: for each link from another kind (a job's customer, a memo's job), how
1271
+ * many rows point here and the newest of them (`limit`, default 20, at most 100), read as query()
1272
+ * reads that kind. Null when there is no such row. The account's token, crm:read.
1273
+ *
1274
+ * @example
1275
+ * const related = await db.related('contact', contactId);
1276
+ * for (const r of related ?? []) console.log(r.kind, r.link, r.count);
1277
+ */
1278
+ related(kind: KindName, id: string, options?: {
1279
+ limit?: number;
1280
+ }): Promise<RelatedRows[] | null>;
1154
1281
  /** The app kinds this account has defined. The account's token, crm:read, every plan; never an app key. */
1155
1282
  kinds(): Promise<KindDescription[]>;
1156
1283
  /**
@@ -1210,6 +1337,18 @@ export declare class AwesomateClient {
1210
1337
  write<K extends KindName>(kind: K, data: Partial<RowOf<K>> | Record<string, unknown>, options?: WriteOptions): Promise<string>;
1211
1338
  /** Archive a record: it leaves every read and is kept for a restore. Needs what write() needs. */
1212
1339
  archive<K extends KindName>(kind: K, id: string): Promise<void>;
1340
+ /**
1341
+ * Create or change the record that mirrors one in another system, found by that system's name and
1342
+ * its id there (`ref.system`, lower case like `xero`; `ref.externalId`), so a sync run twice changes
1343
+ * the record instead of making a second one. The same checks as write(). A record with that id that
1344
+ * was archived is refused (`conflict`): a sync never undoes an archive or makes a second copy. Companies
1345
+ * are refused here: use companies.upsert(), which matches by ABN and name. The account's token only
1346
+ * (never an app key), crm:write, Support Plus and above.
1347
+ *
1348
+ * @example
1349
+ * const { id, created } = await db.upsertByRef('job', { system: 'servicem8', externalId: 'JOB-0042' }, { title: 'Possum in the roof' }, { links: { customer: contactId } });
1350
+ */
1351
+ upsertByRef<K extends KindName>(kind: K, ref: RecordRef, data?: Partial<RowOf<K>> | Record<string, unknown>, options?: Omit<WriteOptions, 'id'>): Promise<UpsertByRefResult>;
1213
1352
  /** The saved queries on this account. crm:read, every plan: the account's token or an app key. */
1214
1353
  queries(): Promise<SavedQueryDescription[]>;
1215
1354
  /**
@@ -1266,6 +1405,26 @@ export declare class AwesomateClient {
1266
1405
  * the business map on the account.
1267
1406
  */
1268
1407
  businessMapRole(slug: string): Promise<BusinessMapRoleDetail>;
1408
+ /**
1409
+ * The business's KPIs: each a number with a target and someone responsible for it, with where it
1410
+ * sits, who answers for it, its light against the target for the last complete period, the period
1411
+ * so far, a trend of up to 12 periods and where its figure comes from. A person's own key reads only the KPIs
1412
+ * they would see in the hub. `period` reads every KPI by the week, month or quarter (never finer than
1413
+ * its own period); left out, each by its own. hosting:read, every plan; `available` is false, with a
1414
+ * `reason`, where the account has no KPIs to read.
1415
+ */
1416
+ businessKpis(opts?: {
1417
+ period?: 'week' | 'month' | 'quarter';
1418
+ }): Promise<BusinessKpis>;
1419
+ /**
1420
+ * Add figures for the business's KPIs from your own tool or script: totals per day, week or month,
1421
+ * into the business's own Your numbers, where a KPI reads them by name. Up to 100 a call; a figure
1422
+ * sent again for the same period replaces the one before. Each figure name belongs to the first
1423
+ * source that sends it (`serverCode: 'figure_owned'` for another). Totals only: a label that looks
1424
+ * like an email address, phone number or web address is refused. knowledge:write, Support Plus and
1425
+ * above (`feature_unavailable` below that), and Your numbers switched on for the business.
1426
+ */
1427
+ addFigures(input: AddFigures): Promise<FiguresAdded>;
1269
1428
  /**
1270
1429
  * The business map in its first shape, where a department is a `division`, a sub-department a
1271
1430
  * `department` and a role a `job`. hosting:read, the business map on the account.
@@ -1420,6 +1579,7 @@ export interface BusinessMapRole {
1420
1579
  headsDepartment: boolean;
1421
1580
  /** Runs its sub-department: the person responsible for it, and for its KPIs. */
1422
1581
  headsSubDepartment: boolean;
1582
+ /** The sub-department's key (see BusinessMapSubDepartment.no): match it there for the name, never show it. */
1423
1583
  subDepartmentNo: number | null;
1424
1584
  reportsTo: {
1425
1585
  id: number;
@@ -1436,17 +1596,91 @@ export interface BusinessMapRole {
1436
1596
  level: BusinessMapLevel;
1437
1597
  exceptions: string | null;
1438
1598
  }>;
1599
+ /**
1600
+ * Its helpers. `look` says what the hub draws: an Awesomate agent's picture URL, a full link to the
1601
+ * hub (null: the hub draws its initials; always null when the account does not have Agents) and
1602
+ * whether it is on (null when that could not be read), an n8n agent's icon, an automation's bolt.
1603
+ */
1439
1604
  helpers: Array<BusinessMapHelper & {
1440
1605
  id: number;
1441
1606
  supervisor: string;
1442
1607
  level: BusinessMapLevel | null;
1443
1608
  exceptions: string | null;
1444
1609
  minutesSavedPerRun: number | null;
1610
+ look: {
1611
+ kind: 'awesomate' | 'n8n' | 'automation' | 'external';
1612
+ picture: string | null;
1613
+ state: 'on' | 'off' | null;
1614
+ };
1445
1615
  }>;
1446
1616
  /** The tag this role's procedures carry in 1Brain, such as 'job:quotes'. The map keeps no procedure text. */
1447
1617
  proceduresTag: string;
1448
1618
  /** Quarterly priorities on this role, every quarter. */
1449
1619
  priorities: BusinessMapPriority[];
1620
+ /** Its responsibilities as processes, in the same order: what writes each down and who does it. */
1621
+ processes: BusinessMapProcess[];
1622
+ /** How many processes it has, how many are written down, how many automated. */
1623
+ progress: {
1624
+ processes: number;
1625
+ written: number;
1626
+ automated: number;
1627
+ };
1628
+ /** Ready to hand over: the owner is still responsible for it and every process is written down or automated. */
1629
+ ready: boolean;
1630
+ /** The one line the map shows for it ("Booking reminders failed twice this week"), or null. */
1631
+ flag: string | null;
1632
+ }
1633
+ /** The one next step the map suggests, with its button's words and what the button does. */
1634
+ export interface BusinessMapNextStep {
1635
+ title: string;
1636
+ detail: string;
1637
+ button: string;
1638
+ action: {
1639
+ kind: 'hand_over';
1640
+ roleId: number;
1641
+ roleSlug: string;
1642
+ membershipId: number | null;
1643
+ } | {
1644
+ kind: 'open_role';
1645
+ roleId: number;
1646
+ roleSlug: string;
1647
+ } | {
1648
+ kind: 'link';
1649
+ path: string;
1650
+ } | {
1651
+ kind: 'add_role';
1652
+ departmentNo: number;
1653
+ } | {
1654
+ kind: 'edit_path';
1655
+ } | {
1656
+ kind: 'place_helpers';
1657
+ };
1658
+ }
1659
+ /**
1660
+ * One of a role's processes (a responsibility). Written down: a 1Brain page on the role's own list of
1661
+ * 1Brain systems is linked to it. Automated: an agent or automation on the role does it, at any level.
1662
+ */
1663
+ export interface BusinessMapProcess {
1664
+ text: string;
1665
+ level: BusinessMapLevel;
1666
+ /** The 1Brain page that writes it down, by its ids and title; null when it is not written down yet. */
1667
+ procedure: {
1668
+ accountId: number;
1669
+ itemId: number;
1670
+ title: string;
1671
+ } | null;
1672
+ /** Who does it: the role's holder, or a helper on the role. */
1673
+ doneBy: {
1674
+ kind: 'holder';
1675
+ name: string | null;
1676
+ } | {
1677
+ kind: 'helper';
1678
+ helperId: number;
1679
+ label: string;
1680
+ helperKind: BusinessMapHelper['kind'];
1681
+ };
1682
+ /** How far whoever does it goes alone, in words: a helper's level, else the role's limits, else "A person". */
1683
+ mayActAlone: string;
1450
1684
  }
1451
1685
  /** A quarterly priority on a role: one of the few things it must move this quarter. */
1452
1686
  export interface BusinessMapPriority {
@@ -1465,6 +1699,8 @@ export interface BusinessMapPathStep {
1465
1699
  position: number;
1466
1700
  label: string;
1467
1701
  departmentNo: 1 | 2 | 3 | 4 | 5 | 6 | 7;
1702
+ /** The department's plain name ("Sales"): what people read. */
1703
+ name: string;
1468
1704
  verb: string;
1469
1705
  /** The role that looks after the step, when the owner named one. */
1470
1706
  role: {
@@ -1480,11 +1716,25 @@ export interface BusinessMapPathStep {
1480
1716
  /** Where this step hands over to the next, in the owner's words. */
1481
1717
  handoffRule: string | null;
1482
1718
  }
1483
- /** A sub-department and who runs it: the holder of the role that heads it, else whoever runs the department (byDefault). */
1719
+ /**
1720
+ * A sub-department and who runs it: the holder of the role that heads it, else whoever runs the
1721
+ * department (byDefault). Each business names, orders and adds its own, so they come in the
1722
+ * business's order and by its names.
1723
+ */
1484
1724
  export interface BusinessMapSubDepartment {
1725
+ /**
1726
+ * A key, never shown to people: 1 to 21 for a standard sub-department, 1001 and up for one the
1727
+ * owner added. A role's subDepartmentNo is this key.
1728
+ */
1485
1729
  no: number;
1730
+ /** The business's name for it: show this. */
1486
1731
  name: string;
1487
- gloss: string;
1732
+ /** The standard name, or null for one the owner added. Differs from name when the owner renamed it. */
1733
+ standardName: string | null;
1734
+ /** The owner added it. */
1735
+ custom: boolean;
1736
+ /** What a standard one covers; null for one the owner added. */
1737
+ gloss: string | null;
1488
1738
  runBy: {
1489
1739
  name: string;
1490
1740
  byDefault: boolean;
@@ -1513,6 +1763,7 @@ export interface BusinessMapDepartment {
1513
1763
  helpers: Array<BusinessMapHelper & {
1514
1764
  placedBy: string;
1515
1765
  }>;
1766
+ /** In the business's order, by its names. */
1516
1767
  subDepartments: BusinessMapSubDepartment[];
1517
1768
  ideas: Array<{
1518
1769
  label: string;
@@ -1527,12 +1778,160 @@ export interface BusinessMapDepartment {
1527
1778
  question: string;
1528
1779
  /** 1Brain Departments whose procedures belong in this department. */
1529
1780
  oneBrainDepartments: string[];
1530
- /** People on the map who hold no role yet. They sit in Form (department 1) for display; empty for every other department. */
1781
+ /** People on the map who hold no role yet. They sit in Team and systems (department 1) for display; empty for every other department. */
1531
1782
  noRoleYet: Array<{
1532
1783
  id: number;
1533
1784
  name: string;
1534
1785
  }>;
1535
1786
  }
1787
+ /**
1788
+ * A KPI's light. Say its word beside any colour: Act now, Watch, On track, Late, Not a KPI yet.
1789
+ * `not_due` is no light at all: the KPI is new and its first figure is not due yet, so it is never Late
1790
+ * before then; `lightWord` says when ("First figure due Mon 19 Oct") and `firstDue` what brings it.
1791
+ */
1792
+ export type KpiLight = 'act_now' | 'watch' | 'on_track' | 'late' | 'no_target' | 'not_due';
1793
+ /** One period of a KPI's figures: first and last day (YYYY-MM-DD), a short label, its figure and target. */
1794
+ export interface KpiPeriodFigure {
1795
+ from: string;
1796
+ to: string;
1797
+ label: string;
1798
+ /** null: no figure for that period. */
1799
+ figure: number | null;
1800
+ target: number | null;
1801
+ }
1802
+ /** One KPI, as businessKpis() returns it. */
1803
+ export interface BusinessKpi {
1804
+ id: string;
1805
+ /** The hub's own KPIs only: fixed when it is added. */
1806
+ slug?: string;
1807
+ name: string;
1808
+ /** false: it points at a place the map no longer has. */
1809
+ onMap: boolean;
1810
+ place: {
1811
+ level: 'department' | 'sub_department' | 'role' | null;
1812
+ department: number | null;
1813
+ subDepartment: number | null;
1814
+ role: string | null;
1815
+ roleTitle: string | null;
1816
+ };
1817
+ /** Who answers for it now; forNow: nobody holds it, so it falls to whoever runs the department. */
1818
+ responsible: {
1819
+ name: string;
1820
+ forNow: boolean;
1821
+ } | null;
1822
+ light: KpiLight;
1823
+ lightWord: string;
1824
+ /** The judged period's figure and target (the last complete period). */
1825
+ figure: number | null;
1826
+ target: number | null;
1827
+ period: {
1828
+ from: string;
1829
+ to: string;
1830
+ } | null;
1831
+ kind: 'lead' | 'result' | null;
1832
+ critical: boolean;
1833
+ format: string | null;
1834
+ /** The rest is on the hub's own KPIs, not on those kept in Reports. */
1835
+ frequency?: 'daily' | 'weekly' | 'monthly';
1836
+ direction?: 'up' | 'down';
1837
+ aggregation?: 'sum' | 'mean' | 'last';
1838
+ /** The target per its own period, as typed: 40 for "40 a week". null: not a KPI yet. */
1839
+ targetPerPeriod?: number | null;
1840
+ targetWords?: string | null;
1841
+ watchPct?: number;
1842
+ source?: 'contacts' | 'collected' | 'entered' | 'other';
1843
+ /** "Entered weekly by Dave, from a task". */
1844
+ sourceLine?: string;
1845
+ sharedEveryone?: boolean;
1846
+ /** Why the reader sees it, in words. */
1847
+ because?: string[];
1848
+ /** The period so far: its figure and the target to date. Never lit. */
1849
+ soFar?: {
1850
+ period: {
1851
+ start: string;
1852
+ end: string;
1853
+ };
1854
+ value: number | null;
1855
+ targetToDate: number | null;
1856
+ };
1857
+ /** Up to 12 complete periods before this one, oldest first, starting where tracking began: shorter, or empty, for a new KPI. */
1858
+ trend?: KpiPeriodFigure[];
1859
+ /** "No figure since 6 Oct", "No figure yet", or null. */
1860
+ figureNote?: string | null;
1861
+ /** Set when the light is Late: since when, and what it needs in a sentence. */
1862
+ late?: {
1863
+ since: string | null;
1864
+ needs: string;
1865
+ task: null;
1866
+ } | null;
1867
+ /** Before last period's figure is due: when it is due. */
1868
+ dueNote?: string | null;
1869
+ /**
1870
+ * Set while the light is `not_due`: the day its first figure is due (YYYY-MM-DD), the words to show
1871
+ * ("First figure due Mon 19 Oct"), and what brings the figure ("No figure yet. Dave enters it each week,
1872
+ * from a task."). `arrived`: a figure for its first period is in already (entered before that period
1873
+ * ended), so `day` is the day it lights up, `words` read "Lights up Mon 19 Oct" and `note` says so.
1874
+ */
1875
+ firstDue?: {
1876
+ day: string;
1877
+ words: string;
1878
+ note: string;
1879
+ arrived: boolean;
1880
+ } | null;
1881
+ }
1882
+ /** One figure for addFigures(): a total for a period, under a named figure a KPI can read. */
1883
+ export interface Figure {
1884
+ /** The named figure, two to four dotted parts in lower case: `xero.cash_received`. */
1885
+ key: string;
1886
+ /** Its name for people, when the key's last part does not say it well. */
1887
+ label?: string;
1888
+ unit: 'count' | 'money' | 'quantity';
1889
+ /** With money: a three-letter code, `AUD`. */
1890
+ currency?: string;
1891
+ /** How periods combine into a longer one: `sum` (the default), `mean`, or `last` for a balance or a rating. */
1892
+ aggregation?: 'sum' | 'mean' | 'last';
1893
+ grain: 'day' | 'week' | 'month';
1894
+ /** Any date in the period (`2026-10-08`), or an ISO time, read in the business's own time zone. Weeks start Monday. */
1895
+ period: string;
1896
+ value: number;
1897
+ }
1898
+ /** What addFigures() sends: where the figures come from, and the figures. */
1899
+ export interface AddFigures {
1900
+ /** The tool or script the figures come from, in a few plain words: `Xero`. */
1901
+ source: {
1902
+ name: string;
1903
+ };
1904
+ figures: Figure[];
1905
+ }
1906
+ /** What addFigures() answers: how many were added, and the Your numbers tables they went to. */
1907
+ export interface FiguresAdded {
1908
+ added: number;
1909
+ source: {
1910
+ name: string;
1911
+ slug: string;
1912
+ };
1913
+ /** One table per source and grain, `collected-<source>-<grain>`. */
1914
+ tables: Array<{
1915
+ slug: string;
1916
+ grain: 'day' | 'week' | 'month';
1917
+ figures: number;
1918
+ periods: number;
1919
+ }>;
1920
+ }
1921
+ /** The business's KPIs, as businessKpis() returns them. */
1922
+ export type BusinessKpis = {
1923
+ available: false;
1924
+ reason: 'not_connected' | 'no_reports' | 'unreadable';
1925
+ } | {
1926
+ available: true;
1927
+ /** Where they are kept: the hub's own until the account links Reports, then Reports'. */
1928
+ store: 'hub' | 'buddzee';
1929
+ truncated: boolean;
1930
+ period?: 'week' | 'month' | 'quarter' | null;
1931
+ /** The hub's own: whether figures could be read from Your numbers. */
1932
+ figures?: 'ok' | 'off' | 'no_workspace' | 'not_configured' | 'unreadable';
1933
+ kpis: BusinessKpi[];
1934
+ };
1536
1935
  /** The business map, as businessMap() returns it. No email addresses. */
1537
1936
  export interface BusinessMap {
1538
1937
  version: 2;
@@ -1591,6 +1990,26 @@ export interface BusinessMap {
1591
1990
  departmentsWithHelpers: number;
1592
1991
  roles: number;
1593
1992
  };
1993
+ /**
1994
+ * How far the business has got: the roles the owner is still responsible for (their own included) of
1995
+ * every role, and the processes written down and automated, each of every process.
1996
+ */
1997
+ progress: {
1998
+ rolesYouHold: {
1999
+ held: number;
2000
+ of: number;
2001
+ };
2002
+ written: {
2003
+ done: number;
2004
+ of: number;
2005
+ };
2006
+ automated: {
2007
+ done: number;
2008
+ of: number;
2009
+ };
2010
+ };
2011
+ /** The one next step: a ready role to hand over first, else the most important gap; null when there is none. */
2012
+ nextStep: BusinessMapNextStep | null;
1594
2013
  /** Sources that could not be read just now: a missing helper may simply not have been read. */
1595
2014
  unavailable: string[];
1596
2015
  }
@@ -1653,7 +2072,12 @@ export interface BusinessMapRoleDetail {
1653
2072
  levelLabel: string;
1654
2073
  exceptions: string | null;
1655
2074
  }>;
2075
+ /** Each responsibility as a process, in the same order. */
2076
+ processes: BusinessMapProcess[];
2077
+ progress: BusinessMapRole['progress'];
2078
+ /** `id` is what a process's `doneBy.helperId` and a proposal's `doneByHelperId` name. */
1656
2079
  helpers: Array<{
2080
+ id: number;
1657
2081
  kind: BusinessMapHelper['kind'];
1658
2082
  ref: string;
1659
2083
  label: string;
@@ -1662,6 +2086,7 @@ export interface BusinessMapRoleDetail {
1662
2086
  levelLabel: string | null;
1663
2087
  exceptions: string | null;
1664
2088
  instructionLine: string;
2089
+ look: BusinessMapRole['helpers'][number]['look'];
1665
2090
  }>;
1666
2091
  }
1667
2092
  /**
@@ -1731,11 +2156,13 @@ export interface BusinessMapDivision {
1731
2156
  helpers: Array<BusinessMapHelper & {
1732
2157
  placedBy: string;
1733
2158
  }>;
1734
- /** The sub-departments. */
2159
+ /** The sub-departments, in the business's order: standardName and custom as on BusinessMapSubDepartment. */
1735
2160
  departments: Array<{
1736
2161
  no: number;
1737
2162
  name: string;
1738
- gloss: string;
2163
+ standardName?: string | null;
2164
+ custom?: boolean;
2165
+ gloss: string | null;
1739
2166
  runBy: {
1740
2167
  name: string;
1741
2168
  byDefault: boolean;
@@ -1930,6 +2357,19 @@ export interface WriteOptions {
1930
2357
  /** Set a link by its key ('<id>'), or end it (null). */
1931
2358
  links?: Record<string, string | null>;
1932
2359
  }
2360
+ /** Which record in another system a record mirrors, for upsertByRef(). */
2361
+ export interface RecordRef {
2362
+ /** That system's name, lower case: letters, digits, `_`, `.` or `-`, at most 40 characters (`xero`, `shopify`). */
2363
+ system: string;
2364
+ /** The record's id in that system, 1 to 200 characters. */
2365
+ externalId: string;
2366
+ }
2367
+ /** What upsertByRef() did. */
2368
+ export interface UpsertByRefResult {
2369
+ id: string;
2370
+ /** True when the record was made; false when the one with that id changed. */
2371
+ created: boolean;
2372
+ }
1933
2373
  /** The type of a saved query's or recipe's parameter. */
1934
2374
  export type ParamType = 'text' | 'number' | 'date' | 'datetime' | 'boolean' | 'uuid' | 'text_list' | 'number_list';
1935
2375
  /** One parameter of a saved query or recipe. */
@@ -2101,7 +2541,7 @@ export interface Schema {
2101
2541
  max_limit: number;
2102
2542
  statement_timeout_ms: number;
2103
2543
  };
2104
- /** With the account's token: the time zone dates are read in unless a query says otherwise. */
2544
+ /** With the account's token: the time zone dates are read in unless a query says otherwise (the business's own, Sydney if none is set). */
2105
2545
  default_timezone?: string;
2106
2546
  /** With an app key: whether it can only read or can also write. */
2107
2547
  access?: 'read' | 'write';
@@ -2351,6 +2791,118 @@ export interface ContactTagsResult {
2351
2791
  /** Tags taken off them. */
2352
2792
  removed: number;
2353
2793
  }
2794
+ /** One link pointing at a row, from related(). */
2795
+ export interface RelatedRows {
2796
+ /** The kind the link starts from. */
2797
+ kind: string;
2798
+ /** That kind's name as the owner wrote it, plural ("Jobs"): show this beside the label. */
2799
+ kind_label: string;
2800
+ /** The link's key on that kind (its rows carry <link>_id). A company's people come as kind contact, link company. */
2801
+ link: string;
2802
+ /** What the link is called from this side, as its maker named it ("has" when they did not). */
2803
+ label: string;
2804
+ /** True when this link could not be read just now: count and rows are empty, never zero. */
2805
+ unavailable?: boolean;
2806
+ /** Fields and sections of this kind your role cannot see, when any were left out. */
2807
+ not_shown_to_you?: NotShownToYou;
2808
+ /** How many rows point here, up to 10,000 (count_capped past it). */
2809
+ count: number;
2810
+ count_capped: boolean;
2811
+ /** The newest of them, only columns query() would show. */
2812
+ rows: Array<Record<string, unknown>>;
2813
+ /** Pass to query() with where { <link>_id: id } and after, for more. */
2814
+ next: string | null;
2815
+ }
2816
+ /** How a person asked to stop getting email, for contacts.unsubscribe(). */
2817
+ export type ContactStopHow = 'reply' | 'phone' | 'in_person' | 'in_writing' | 'other';
2818
+ /** What contacts.unsubscribe() recorded. */
2819
+ export interface ContactUnsubscribeResult {
2820
+ id: string;
2821
+ /** False when it was already recorded. */
2822
+ changed: boolean;
2823
+ /** Whether email reaches them now. */
2824
+ email_status: ContactEmailStatus;
2825
+ /** The list they left, or null for every email. */
2826
+ list: string | null;
2827
+ how: ContactStopHow;
2828
+ /** When they asked, ISO 8601. */
2829
+ asked_at: string;
2830
+ }
2831
+ /** What erasing a person would take, from contacts.erasurePreview(): counts only, never values. */
2832
+ export interface ContactErasureCounts {
2833
+ /** Their email addresses and phone numbers. */
2834
+ email_addresses: number;
2835
+ phone_numbers: number;
2836
+ /** Of their numbers, those someone else also holds: texts to them stop for that person too. */
2837
+ phone_numbers_shared: number;
2838
+ /** Details on their page with a value: name, company, phone, time zone and the owner's fields. */
2839
+ fields: number;
2840
+ tags: number;
2841
+ /** Email lists they are on. */
2842
+ lists: number;
2843
+ /** Their consent records with an IP address or browser: the records stay as proof, those details go. */
2844
+ consent_ip_details: number;
2845
+ /** Emails sent to them, and their opens and clicks. */
2846
+ emails_sent: number;
2847
+ opens_and_clicks: number;
2848
+ series_runs: number;
2849
+ /** Calls with them, and calls from their numbers that named nobody (a shared line's among them). */
2850
+ calls: number;
2851
+ calls_shared_number: number;
2852
+ /** What the agent remembered about them (0 or 1). */
2853
+ call_memory: number;
2854
+ /** Bookings: the answers they gave go, the times stay. */
2855
+ bookings: number;
2856
+ /** Support tickets that are theirs, and how each was found (a ticket can be found more than one way). */
2857
+ tickets: number;
2858
+ tickets_by_link: number;
2859
+ tickets_by_email: number;
2860
+ tickets_by_number: number;
2861
+ /** Someone else's tickets that carry their address or number: only that comes off. */
2862
+ tickets_of_someone_else: number;
2863
+ /** Tickets from a number someone else also holds: left alone. */
2864
+ tickets_shared_number: number;
2865
+ /** Their tickets that someone else raised: that person's own address, number and name stay. */
2866
+ tickets_customer_kept: number;
2867
+ /** Messages they wrote, their attachments, emails from them held as suspect, and stored raw copies. */
2868
+ messages: number;
2869
+ /** Customers' messages on their tickets that are someone else's, or that can't be told to be theirs: they stay. */
2870
+ messages_not_theirs: number;
2871
+ attachments: number;
2872
+ quarantined_emails: number;
2873
+ raw_emails: number;
2874
+ /** Stored emails also filed on someone else's record: kept for it. */
2875
+ raw_emails_shared: number;
2876
+ /** Other records linked to them, by kind; the links end and the records keep their own fields. */
2877
+ linked_records: Array<{
2878
+ kind: string;
2879
+ label: string;
2880
+ records: number;
2881
+ required: number;
2882
+ }>;
2883
+ /** Of those, records that must have a person and will need a new one before their next change. */
2884
+ linked_required: number;
2885
+ /** Their sign-in to the account's apps, and sign-in links sent to them. */
2886
+ app_sign_ins: number;
2887
+ /** Other sign-in addresses linked to them, blocked with theirs. */
2888
+ app_addresses_blocked: number;
2889
+ sign_in_links: number;
2890
+ /** Their rows in past imports. */
2891
+ import_rows: number;
2892
+ }
2893
+ /** What contacts.erasurePreview() answers. Nothing has been erased. */
2894
+ export interface ContactErasurePreview {
2895
+ /** The person's id, and whether they were removed from Contacts. Never their name or address: the Owner sees who it is in the hub. */
2896
+ contact: {
2897
+ id: string;
2898
+ removed: boolean;
2899
+ };
2900
+ counts: ContactErasureCounts;
2901
+ /** Where an Owner finishes it in the hub, signed in as themselves. Null when it can't be finished there on this account yet: pass on `note`. */
2902
+ finishInHub: string | null;
2903
+ /** What happens next, in a sentence to pass on. */
2904
+ note: string;
2905
+ }
2354
2906
  /** The kinds of contact field contacts.createField() makes. */
2355
2907
  export type ContactFieldType = 'text' | 'long_text' | 'number' | 'money' | 'date' | 'yes_no' | 'choice' | 'choices';
2356
2908
  /** What contacts.createField() takes. */
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@
26
26
  * Docs: https://hub.awesomate.ai/docs/sdk/
27
27
  */
28
28
  /** This package's version, sent to the hub with every server call. */
29
- export const VERSION = '0.34.0';
29
+ export const VERSION = '0.51.0';
30
30
  const DEFAULT_BASE = 'https://hub.awesomate.ai';
31
31
  /** Every record and contact id is a UUID; anything else can name no row. */
32
32
  const RECORD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -62,6 +62,8 @@ export class AwesomateError extends Error {
62
62
  * being moved). Wait at least that long, then try once more; retrying at once only meets the same answer.
63
63
  */
64
64
  retryAfter;
65
+ /** With serverCode unknown_role (`tasks.give({ role })` naming a role not on the business map): the map's roles, by key and name. */
66
+ roles;
65
67
  constructor(
66
68
  /** Why it failed. */
67
69
  code, message,
@@ -94,6 +96,8 @@ export class AwesomateError extends Error {
94
96
  this.requiredPlan = details.requiredPlan;
95
97
  if (details.retryAfter !== undefined)
96
98
  this.retryAfter = details.retryAfter;
99
+ if (details.roles !== undefined)
100
+ this.roles = details.roles;
97
101
  }
98
102
  }
99
103
  /** Every code the hub uses for "the account has switched this off in its privacy settings". */
@@ -153,6 +157,12 @@ function refusal(status, json) {
153
157
  ?? (Array.isArray(json.requiredPlans) ? nonEmpty(json.requiredPlans[0]) : undefined);
154
158
  if (required)
155
159
  details.requiredPlan = required;
160
+ if (Array.isArray(json.roles)) {
161
+ details.roles = json.roles
162
+ .filter((r) => Boolean(r) && typeof r === 'object')
163
+ .map((r) => ({ slug: String(r.slug ?? ''), title: String(r.title ?? '') }))
164
+ .filter((r) => r.slug !== '');
165
+ }
156
166
  return {
157
167
  code: errorCode(status, serverCode, json),
158
168
  message: (codeInError ? sentence : error ?? sentence) ?? `The hub answered ${status}.`,
@@ -414,6 +424,7 @@ export class AwesomateClient {
414
424
  *
415
425
  * @example
416
426
  * await db.tasks.give({ title: 'Call Sam back about the leak', forEmail: 'jo@brightwater.example', dueAt: '2026-10-06T09:00:00+11:00' });
427
+ * await db.tasks.give({ title: 'Hot lead: Sam wants a quote today', forEmail: 'jo@brightwater.example', urgent: true });
417
428
  */
418
429
  tasks = {
419
430
  /** The board: waiting now, put off, with someone else, recently decided. `all` shows everyone's, not only yours. hosting:read. */
@@ -431,15 +442,32 @@ export class AwesomateClient {
431
442
  * token may not see reads as not found (serverCode `gone`). hosting:read.
432
443
  */
433
444
  decisionRecord: (taskId) => this.tasksCall('GET', `/api/my-tasks/decisions/${encodeURIComponent(String(taskId))}/record`),
434
- /** Give a task. The person gets it on their board, and an email when they are not the one giving it. hosting:manage. */
445
+ /**
446
+ * Give a task. The person gets it on their board, an email when they are not the one giving it,
447
+ * and a card in any chat they linked, within their quiet hours and daily limit (`urgent` goes
448
+ * straight away). `urgent` in the result says whether it went as urgent: a person's sixth urgent
449
+ * task in a day goes as an ordinary one, and `note` says so. hosting:manage.
450
+ */
435
451
  give: async (task) => {
436
452
  const r = await this.tasksCall('POST', '/api/my-tasks/tasks', task);
437
- return { key: r.key, id: r.id, holder: r.holder, emailed: r.emailed };
453
+ return { key: r.key, id: r.id, holder: r.holder, emailed: r.emailed, urgent: r.urgent === true, ...(r.role ? { role: r.role } : {}), ...(r.note ? { note: r.note } : {}) };
438
454
  },
439
- /** Change a given task: whoever wrote it, or the owner. hosting:manage. */
455
+ /**
456
+ * Change a given task: whoever wrote it, or the owner. `urgent: false` takes urgent off; a task can
457
+ * only be marked urgent when it is given. hosting:manage.
458
+ */
440
459
  update: async (id, patch) => {
441
460
  await this.tasksCall('PATCH', `/api/my-tasks/tasks/${encodeURIComponent(String(id))}`, patch);
442
461
  },
462
+ /**
463
+ * Take a role's task (a card with `role.mayTake`): it becomes the owner's, and everyone else in
464
+ * the role sees who took it. Only someone in the role can take it, so this works for the owner
465
+ * only when they are in that role (serverCode `not_in_role` otherwise). Someone who got there
466
+ * first answers `taken`, with their name in the message. hosting:manage.
467
+ */
468
+ take: async (id) => {
469
+ await this.tasksCall('POST', `/api/my-tasks/tasks/${encodeURIComponent(String(id))}/take`);
470
+ },
443
471
  /** Mark a given task done: the person who has it, whoever wrote it, or the owner. hosting:manage. */
444
472
  done: async (id) => {
445
473
  await this.tasksCall('POST', `/api/my-tasks/tasks/${encodeURIComponent(String(id))}/done`);
@@ -786,6 +814,37 @@ export class AwesomateClient {
786
814
  addTags: async (id, tags) => this.contactsWrite('POST', `/${contactIdOf(id)}/tags`, { add: tags }),
787
815
  /** Take tags off a person, up to 20 a call. Answers how many they had. */
788
816
  removeTags: async (id, tags) => this.contactsWrite('POST', `/${contactIdOf(id)}/tags`, { remove: tags }),
817
+ /**
818
+ * Record that the person asked to stop getting email, by reply, phone, in person or in writing
819
+ * (`how`), at the time they asked (`askedAt`, default now, at most 30 days back). With no `list`
820
+ * it stops every email, booking confirmations and other service emails included; with a list's
821
+ * key, that list only. Recorded as the person's own withdrawal, so a later import never puts
822
+ * them back. Answers `changed: false` when it was already recorded. The law gives 5 business
823
+ * days from when they asked. A person with no email address is refused (validation).
824
+ */
825
+ unsubscribe: async (id, options) => {
826
+ const askedAt = options.askedAt instanceof Date ? options.askedAt.toISOString() : options.askedAt;
827
+ return this.contactsWrite('POST', `/${contactIdOf(id)}/unsubscribe`, {
828
+ how: options.how, ...(options.list !== undefined ? { list: options.list } : {}), ...(askedAt !== undefined ? { asked_at: askedAt } : {}),
829
+ });
830
+ },
831
+ /**
832
+ * Prepare an erasure, for when someone asks for their details to be deleted. Answers what
833
+ * erasing the person would take (`counts`) and `finishInHub`, the hub link where an Owner of the
834
+ * business finishes it, signed in as themselves, by typing the person's email address. Nothing
835
+ * is erased here: no token can erase anyone, because an erasure can't be undone. Only an Owner's
836
+ * token may prepare one (403 `owner_only`). Null when there is no such person.
837
+ */
838
+ erasurePreview: async (id) => {
839
+ if (this.appKey)
840
+ throw contactsNeedAccount();
841
+ if (!RECORD_ID.test(String(id)))
842
+ return null;
843
+ const r = await nullIfMissing(this.request('GET', `/api/my-crm/v1/contacts/${encodeURIComponent(String(id))}/erasure`));
844
+ if (!r)
845
+ return null;
846
+ return { contact: r.contact, counts: r.counts, finishInHub: r.finish_in_hub, note: r.note };
847
+ },
789
848
  /**
790
849
  * Add a contact field. The hub makes its key from the label with a random part and answers it:
791
850
  * use that key. Nothing here changes a field that exists. `ai_may_read` decides, once and now,
@@ -1010,6 +1069,22 @@ export class AwesomateClient {
1010
1069
  throw err;
1011
1070
  }
1012
1071
  }
1072
+ /**
1073
+ * What points at one row: for each link from another kind (a job's customer, a memo's job), how
1074
+ * many rows point here and the newest of them (`limit`, default 20, at most 100), read as query()
1075
+ * reads that kind. Null when there is no such row. The account's token, crm:read.
1076
+ *
1077
+ * @example
1078
+ * const related = await db.related('contact', contactId);
1079
+ * for (const r of related ?? []) console.log(r.kind, r.link, r.count);
1080
+ */
1081
+ async related(kind, id, options = {}) {
1082
+ if (!RECORD_ID.test(String(id)))
1083
+ return null;
1084
+ const qs = options.limit ? `?limit=${options.limit}` : '';
1085
+ const r = await nullIfMissing(this.request('GET', `/api/my-crm/v1/rows/${encodeURIComponent(kind)}/${encodeURIComponent(String(id))}/related${qs}`));
1086
+ return r ? r.related : null;
1087
+ }
1013
1088
  /** The app kinds this account has defined. The account's token, crm:read, every plan; never an app key. */
1014
1089
  async kinds() {
1015
1090
  return (await this.request('GET', '/api/my-crm/v1/kinds')).kinds;
@@ -1087,6 +1162,25 @@ export class AwesomateClient {
1087
1162
  async archive(kind, id) {
1088
1163
  await this.request('POST', '/api/my-crm/v1/call', { fn: 'archive_record', args: { kind, id } }, false);
1089
1164
  }
1165
+ /**
1166
+ * Create or change the record that mirrors one in another system, found by that system's name and
1167
+ * its id there (`ref.system`, lower case like `xero`; `ref.externalId`), so a sync run twice changes
1168
+ * the record instead of making a second one. The same checks as write(). A record with that id that
1169
+ * was archived is refused (`conflict`): a sync never undoes an archive or makes a second copy. Companies
1170
+ * are refused here: use companies.upsert(), which matches by ABN and name. The account's token only
1171
+ * (never an app key), crm:write, Support Plus and above.
1172
+ *
1173
+ * @example
1174
+ * const { id, created } = await db.upsertByRef('job', { system: 'servicem8', externalId: 'JOB-0042' }, { title: 'Possum in the roof' }, { links: { customer: contactId } });
1175
+ */
1176
+ async upsertByRef(kind, ref, data = {}, options = {}) {
1177
+ if (this.appKey)
1178
+ throw new AwesomateError('forbidden', "upsertByRef needs the account's token (amt_pat_...). An app key writes records with write().", 0);
1179
+ const r = await this.request('POST', `/api/my-crm/v1/records/${encodeURIComponent(kind)}/by-ref`, {
1180
+ system: ref.system, external_id: ref.externalId, data, links: options.links ?? {},
1181
+ }, false);
1182
+ return r.result;
1183
+ }
1090
1184
  /** The saved queries on this account. crm:read, every plan: the account's token or an app key. */
1091
1185
  async queries() {
1092
1186
  return (await this.request('GET', '/api/my-crm/v1/queries')).queries;
@@ -1164,6 +1258,29 @@ export class AwesomateClient {
1164
1258
  async businessMapRole(slug) {
1165
1259
  return (await this.request('GET', `/api/my-business/v2/map/roles/${encodeURIComponent(slug)}`)).role;
1166
1260
  }
1261
+ /**
1262
+ * The business's KPIs: each a number with a target and someone responsible for it, with where it
1263
+ * sits, who answers for it, its light against the target for the last complete period, the period
1264
+ * so far, a trend of up to 12 periods and where its figure comes from. A person's own key reads only the KPIs
1265
+ * they would see in the hub. `period` reads every KPI by the week, month or quarter (never finer than
1266
+ * its own period); left out, each by its own. hosting:read, every plan; `available` is false, with a
1267
+ * `reason`, where the account has no KPIs to read.
1268
+ */
1269
+ async businessKpis(opts = {}) {
1270
+ return this.request('GET', `/api/my-business/v2/map/kpis${opts.period ? `?period=${opts.period}` : ''}`);
1271
+ }
1272
+ /**
1273
+ * Add figures for the business's KPIs from your own tool or script: totals per day, week or month,
1274
+ * into the business's own Your numbers, where a KPI reads them by name. Up to 100 a call; a figure
1275
+ * sent again for the same period replaces the one before. Each figure name belongs to the first
1276
+ * source that sends it (`serverCode: 'figure_owned'` for another). Totals only: a label that looks
1277
+ * like an email address, phone number or web address is refused. knowledge:write, Support Plus and
1278
+ * above (`feature_unavailable` below that), and Your numbers switched on for the business.
1279
+ */
1280
+ async addFigures(input) {
1281
+ const r = await this.request('POST', '/api/my-business/v2/figures', input);
1282
+ return { added: r.added, source: r.source, tables: r.tables };
1283
+ }
1167
1284
  /**
1168
1285
  * The business map in its first shape, where a department is a `division`, a sub-department a
1169
1286
  * `department` and a role a `job`. hosting:read, the business map on the account.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/sdk",
3
- "version": "0.34.0",
3
+ "version": "0.51.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",