@awesomate/sdk 0.12.0 → 0.14.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/README.md CHANGED
@@ -1,215 +1,48 @@
1
1
  # @awesomate/sdk
2
2
 
3
- Read your own Awesomate data from Node. Version 0.1 reads your **contact list**: query it,
4
- page through it, count it, with TypeScript types generated from your own fields.
3
+ Build apps on your own Awesomate data. Sign your customers in, show them their records as they change, and let them act, with the rules about who sees what kept in your own database.
4
+
5
+ **Docs: https://hub.awesomate.ai/docs/sdk/** (a quickstart, a tutorial that builds a customer portal, guides, and a reference generated from this package's code). For AI tools: [llms.txt](https://hub.awesomate.ai/docs/sdk/llms.txt).
5
6
 
6
7
  ```bash
7
8
  npm install @awesomate/sdk
8
- npx @awesomate/sdk types --out awesomate.d.ts # uses your Awesomate MCP connection, or AWESOMATE_TOKEN
9
- ```
10
-
11
- ```ts
12
- import { createClient } from '@awesomate/sdk';
13
- import './awesomate'; // the generated file: every query below is typed
14
-
15
- const db = createClient({ token: process.env.AWESOMATE_TOKEN! });
16
-
17
- // People who joined this year, newest first
18
- const { rows, next, applied } = await db.query('contact', {
19
- where: { created_at: { gte: '$YEAR_BEGIN' } },
20
- orderBy: ['created_at', 'desc'],
21
- limit: 50,
22
- });
23
-
24
- // Everyone tagged VIP in one suburb, a page at a time
25
- for await (const person of db.queryAll('contact', { where: { tags: 'VIP', suburb: 'Carindale' }, select: ['first_name', 'email'] })) {
26
- console.log(person.first_name, person.email);
27
- }
28
-
29
- const one = await db.get('contact', rows[0].id); // null if there is none
30
- const byPlan = await db.aggregate({ measures: [{ column: 'people', agg: 'count' }], dimensions: ['plan'] });
31
9
  ```
32
10
 
33
- ## The business itself
34
-
35
- ```js
36
- const biz = await db.business();
37
- biz.name; // 'Acme Plumbing'
38
- for (const g of biz.groups) for (const f of g.facts) console.log(g.title, f.label, f.value, f.status, f.source);
39
- biz.completeness; // { core_set: 7, core_total: 8, missing_core: ['logo'] }
40
- const waiting = await db.businessSuggestions(); // details waiting for the owner's yes
41
- ```
42
-
43
- The details Awesomate keeps about the business: name, what it does, voice, colours, how customers reach it. Each one says where it came from. `confirmed` details are the ones agents and automations use. A `suggestion` waits for the owner, who confirms it on Knowledge, Your business in the hub. This needs the account's token, on every plan, never an app key.
44
-
45
- ## Writing app data (Support Plus and above)
46
-
47
- Define the tables an app keeps, then write to them. There's no migration and no SQL; every value is
48
- checked against the kind (types, choices, required attributes and links).
49
-
50
- ```ts
51
- await db.defineKind({
52
- key: 'job', label: 'Job', label_plural: 'Jobs',
53
- attributes: [
54
- { key: 'title', type: 'text', required: true, readable_by_ai: true },
55
- { key: 'status', type: 'choice', choices: ['open', 'booked', 'done'], readable_by_ai: true },
56
- ],
57
- links: [{ key: 'customer', to: 'contact' }],
58
- });
59
- const id = await db.write('job', { title: 'Possum in the roof', status: 'open' }, { links: { customer: contactId } });
60
- await db.write('job', { status: 'booked' }, { id }); // values not given stay
61
- const jobs = await db.query('job', { where: { customer_id: contactId } });
62
- await db.archive('job', id);
63
- ```
64
-
65
- Only attributes marked `readable_by_ai` come back from `query` and `get`; a `sensitive` attribute
66
- can never be readable. Contacts themselves are not written this way: the contact list keeps its
67
- own consent rules.
68
-
69
- ## What you can read
70
-
71
- The built-in contact details (name, email, phone, company, tags, when they were added) and only
72
- the fields you have marked **readable by AI** under Contacts, Your fields in the hub. A field
73
- marked sensitive is never readable. `db.schema()` lists the columns and says how many are hidden.
74
-
75
- ## The where grammar
76
-
77
- `{ column: value }` means equals; `{ column: { op: value } }` uses an operator: `eq`, `neq`, `gt`,
78
- `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `startsWith`, `like`, `ilike`, `isNull`, and for lists
79
- (tags) `has`, `hasAny`, `hasAll`, `isEmpty`. Combine with `and: [...]`, `or: [...]`, `not: {...}`.
80
- A date (`'2026-09-30'`) against a timestamp means that whole day in your time zone; time
81
- variables `$TODAY`, `$WEEK_BEGIN`, `$MONTH_BEGIN`, `$QUARTER_BEGIN`, `$YEAR_BEGIN` and `$FY_BEGIN`
82
- (1 July) take an offset in their own unit (`$MONTH_BEGIN-1` is the start of last month).
83
-
84
- ## Errors
85
-
86
- Every failure is an `AwesomateError` with a `code`: `unauthenticated`, `forbidden`, `not_found`,
87
- `validation` (with `field`), `consent_blocked`, `rate_limited`, `conflict` or `unavailable`.
88
-
89
- ## Keep the token on a server
90
-
91
- The token is your account's hosting token. Use it in a server, a script or a scheduled job, never
92
- in a browser. For a browser, sign your app's own users in (below).
93
-
94
- ## Your app's server: a key of its own
95
-
96
- An app's own server, or an n8n workflow, should not carry the account's token. Give it a server
97
- key instead: Claude makes one with `awesomate_crm_apps` (`create_key`), read or write, for every
98
- kind or only the ones it names. The key is shown once; put it in the server's environment.
99
-
100
- ```ts
101
- const db = createClient({ token: process.env.AWESOMATE_APP_KEY! }); // ak_...
102
- const { rows } = await db.query('job', { where: { status: 'booked' } });
103
- await db.write('job', { status: 'done' }, { id: rows[0].id });
104
- await db.call('book_job', { customer, title: 'Gutter clean', status: 'booked' });
105
- ```
106
-
107
- A key reads and writes records and runs saved queries and recipes, only for its kinds (a recipe
108
- that writes anything else is refused). It reads every attribute of an app kind, and the contact
109
- list only as Claude does. Writes need Support Plus and above. It cannot change kinds, rules,
110
- queries or recipes, and it never works from a browser. Revoking a key stops it at once; switching
111
- the app off stops all of them. From n8n, send it as `Authorization: Bearer ak_...` to
112
- `https://hub.awesomate.ai/api/sdk/v1/server/rows/query` (and `/call`, `/queries/<key>/run`).
113
-
114
- ## Your app's users sign in (Pro and above)
115
-
116
- Set the app up first (Claude Code: `awesomate_crm_apps`): its name, the origins it runs on, and
117
- whether anyone may sign up or only people you add. That gives you a publishable key, which is not a
118
- secret. Then say who may read and change each kind (`awesomate_crm_kinds`, `set_access`), for
119
- example members read jobs whose customer is them and the memos on those jobs.
11
+ ## In a web page: your app's people (Pro and above)
120
12
 
121
13
  ```ts
122
14
  import { createAppClient } from '@awesomate/sdk';
123
15
 
124
- const app = createAppClient({ publishableKey: 'pk_...' });
16
+ const app = createAppClient({ publishableKey: 'pk_...' }); // not a secret
125
17
 
126
- // Render on every change of person: signed in, signed out, a new role.
127
- app.auth.onChange((u) => render(u));
128
- app.auth.onSignInError((err) => showMessage(err.message));
129
-
130
- // The sign-in page: an email link, no password. The answer is the same for any address.
18
+ app.auth.onChange((user) => render(user));
19
+ render(await app.auth.user()); // someone already signed in on this device
131
20
  await app.auth.signInWithLink(email, { redirectTo: location.href });
132
21
 
133
- // Everywhere else: the same calls as the server client, as this person.
134
- const { rows } = await app.query('job', { orderBy: ['created_at', 'desc'] });
135
- await app.write('forum_post', { body: 'Booked for Tuesday' }, { links: { job: rows[0].id } });
136
- await app.auth.signOut();
137
- ```
138
-
139
- The link signs them in by itself. In a browser the client takes the link from the address bar,
140
- on load or when it arrives in a tab that is already open (only the part after `#` changes, so
141
- the page does not reload), clears it at once and tells `onChange`. A link that has expired or
142
- was already used goes to `onSignInError`. `await app.auth.completeSignIn()` still works and
143
- returns the same sign-in, never a second exchange. Pass `handleSignInLinks: false` to take links
144
- yourself. `onChange` hears about people, not tokens: the routine token refresh is silent.
145
-
146
- ### Lists that keep themselves current
147
-
148
- ```ts
149
- const stop = app.live('job', { orderBy: ['created_at', 'desc'], limit: 50 }, (rows) => render(rows));
150
- // later: stop();
22
+ // As the signed-in person: the database applies the account's rules.
23
+ const stop = app.live('job', { orderBy: ['created_at', 'desc'] }, (rows) => render(rows));
24
+ await app.call('accept_quote', { job: job.id });
151
25
  ```
152
26
 
153
- `live()` calls you with the whole list at first and again whenever a row in it is added, changed,
154
- removed, or stops being one this person may read (a job moved to another customer leaves their
155
- list, with its memos). Changes made anywhere reach the list: in this app, by your team in Claude
156
- Code, by an n8n workflow. One connection serves every list on the client; it reconnects by itself
157
- and takes a fresh copy when it does. Up to 200 rows a list and 20 lists a connection. Pass
158
- `{ onRows, onError }` to hear about a list the server refused (a column that does not exist).
159
- Outside a browser, give it a WebSocket: `createAppClient({ publishableKey, WebSocket })`.
160
-
161
- While a list is open in a browser, the connection also tells the hub whether the tab is in front.
162
- If the account switched on reply emails, someone looking at the app isn't emailed about a reply,
163
- but a tab left open in the background doesn't count as looking, and closing the last one counts as
164
- leaving straight away.
165
-
166
- A conversation can also show who else has it open and who is typing:
27
+ ## On a server: your account's data
167
28
 
168
29
  ```ts
169
- const room = app.here(job.id, (people) => showWhoIsHere(people)); // [{ user, role, name, typing, assistant? }]
170
- messageBox.oninput = () => room.typing(true); // sent at most every few seconds
171
- messageBox.onblur = () => room.typing(false);
172
- // later: room.leave();
173
- ```
174
-
175
- Everyone else with that record open and their tab in front is listed once, and the app's
176
- assistant appears while it writes a reply. The hub checks this person may read the record, and
177
- keeps checking; a refusal goes to the optional third argument, `onError`. Nothing is stored.
178
-
179
- ### Staff find a customer
30
+ import { createClient } from '@awesomate/sdk';
180
31
 
181
- People in an app read no contacts but their own, so by default staff cannot put a job under a
182
- customer. Switch it on per app for the roles that need it (`awesomate_crm_apps`,
183
- `customer_lookup`, never the role customers sign in with). Those roles can then search by name or
184
- email and point a record at any customer:
32
+ const db = createClient({ token: process.env.AWESOMATE_TOKEN! }); // a secret: never in a browser
185
33
 
186
- ```ts
187
- const [jo] = await app.lookupCustomers({ q: 'jo' }); // or { ids: [...] }, up to 50
188
- await app.write('job', { title: 'Gutters' }, { links: { customer: jo.id } });
34
+ const { rows, next } = await db.query('contact', { where: { created_at: { gte: '$YEAR_BEGIN' } }, limit: 50 });
35
+ await db.write('job', { title: 'Possum in the roof', status: 'open' }, { links: { customer: rows[0].id } });
189
36
  ```
190
37
 
191
- Name, email and phone only, and nothing the owner marked sensitive. Anyone else is refused.
192
-
193
- An attribute can also be visible to some roles only (`awesomate_crm_kinds`, `set_visibility`, for
194
- example a cost only `staff` see). For everyone else it comes back `null` and a write that sets it
195
- is refused, whatever the app asks for.
196
-
197
- Some things a customer should do without being able to edit the record: accept their quote, say.
198
- Save it as a write recipe and open it to their role (`awesomate_crm_recipes`, `run_by`, Pro and
199
- above), then:
38
+ Types for your own account's kinds, saved queries and recipes:
200
39
 
201
- ```ts
202
- await app.call('accept_quote', { job: job.id });
40
+ ```bash
41
+ npx @awesomate/sdk types --out awesomate.d.ts
203
42
  ```
204
43
 
205
- The database lets them do exactly the recipe's steps: only its params take their values, every
206
- other value is fixed, and an existing record must be one they can already read. A recipe not
207
- opened to their role is "not found".
44
+ ## What changed
208
45
 
209
- The rules are kept in your own database, which applies them to every read and write, so a mistake
210
- in the app cannot show anyone more than their role allows. A disabled person is refused on their
211
- next call. Sessions are kept in `localStorage`; in a Capacitor app pass `storage` (Preferences) and
212
- call `completeSignIn(url)` with the deep link that opened the app. Your own server can verify
213
- `await app.auth.accessToken()` against `https://hub.awesomate.ai/api/sdk/v1/jwks.json`.
46
+ See the [changelog](https://hub.awesomate.ai/docs/sdk/changelog/).
214
47
 
215
48
  MIT licence.
package/dist/index.d.ts CHANGED
@@ -18,15 +18,19 @@
18
18
  * const stop = app.live('job', {}, (rows) => render(rows)); // and kept current as they change
19
19
  *
20
20
  * The account's token (a hosting PAT with crm:read) is a secret: use it on a server, never in a
21
- * browser. End-user sign-in for browser apps comes in a later version. Reading and running saved
22
- * queries works on every plan; writing app data (kinds, records, saved queries, recipes) needs
23
- * crm:write, Support Plus and above.
21
+ * browser. Reading and running saved queries works on every plan; writing app data (kinds,
22
+ * records, saved queries, recipes) needs crm:write, Support Plus and above.
23
+ *
24
+ * Docs: https://hub.awesomate.ai/docs/sdk/
24
25
  */
25
- export declare const VERSION = "0.7.0";
26
+ /** This package's version, sent to the hub with every server call. */
27
+ export declare const VERSION = "0.14.0";
26
28
  /** Augmented by the generated awesomate.d.ts, so each kind's rows are typed. */
27
29
  export interface Kinds {
28
30
  }
31
+ /** A kind's name: one of yours once the generated types are imported, any string before then. */
29
32
  export type KindName = [keyof Kinds] extends [never] ? string : Extract<keyof Kinds, string>;
33
+ /** A row of kind K: typed from the generated file, or a plain record before then. */
30
34
  export type RowOf<K> = K extends keyof Kinds ? Kinds[K] : Record<string, unknown>;
31
35
  /** Augmented by the generated awesomate.d.ts: each saved query's params and row. */
32
36
  export interface Queries {
@@ -34,18 +38,24 @@ export interface Queries {
34
38
  /** Augmented by the generated awesomate.d.ts: each write recipe's args. */
35
39
  export interface Recipes {
36
40
  }
41
+ /** A saved query's key: one of yours once the generated types are imported, any string before then. */
37
42
  export type QueryName = [keyof Queries] extends [never] ? string : Extract<keyof Queries, string>;
43
+ /** The parameters saved query Q takes. */
38
44
  export type ParamsOf<Q> = Q extends keyof Queries ? (Queries[Q] extends {
39
45
  params: infer P;
40
46
  } ? P : never) : Record<string, unknown>;
47
+ /** The row saved query Q returns. */
41
48
  export type QueryRowOf<Q> = Q extends keyof Queries ? (Queries[Q] extends {
42
49
  row: infer R;
43
50
  } ? R : never) : Record<string, unknown>;
51
+ /** A write recipe's key: one of yours once the generated types are imported, any string before then. */
44
52
  export type RecipeName = [keyof Recipes] extends [never] ? string : Extract<keyof Recipes, string>;
53
+ /** The arguments recipe R takes. */
45
54
  export type ArgsOf<R> = R extends keyof Recipes ? (Recipes[R] extends {
46
55
  args: infer A;
47
56
  } ? A : never) : Record<string, unknown>;
48
- type TextOps<V extends string> = {
57
+ /** Filters on a text column. gt/gte/lt/lte also take time variables such as `$YEAR_BEGIN` on dates. */
58
+ export type TextOps<V extends string> = {
49
59
  eq?: V | null;
50
60
  neq?: V | null;
51
61
  in?: V[];
@@ -60,7 +70,8 @@ type TextOps<V extends string> = {
60
70
  lte?: string;
61
71
  isNull?: boolean;
62
72
  };
63
- type NumberOps = {
73
+ /** Filters on a number column. */
74
+ export type NumberOps = {
64
75
  eq?: number | null;
65
76
  neq?: number | null;
66
77
  gt?: number;
@@ -71,20 +82,29 @@ type NumberOps = {
71
82
  nin?: number[];
72
83
  isNull?: boolean;
73
84
  };
74
- type BoolOps = {
85
+ /** Filters on a yes/no column. */
86
+ export type BoolOps = {
75
87
  eq?: boolean | null;
76
88
  neq?: boolean | null;
77
89
  isNull?: boolean;
78
90
  };
79
- type ListOps<E> = {
91
+ /** Filters on a list column (tags, choices): has one, any or all, or is empty. */
92
+ export type ListOps<E> = {
80
93
  has?: E;
81
94
  hasAny?: E[];
82
95
  hasAll?: E[];
83
96
  isEmpty?: boolean;
84
97
  isNull?: boolean;
85
98
  };
86
- /** What one column accepts in a where clause: a bare value means equals (a list means in, or has all for a list column). */
87
- export type ColumnFilter<V> = V extends Array<infer E> ? E | E[] | ListOps<E> : V extends number ? V | V[] | null | NumberOps : V extends boolean ? V | null | BoolOps : V extends string ? V | V[] | null | TextOps<V> : unknown;
99
+ /**
100
+ * What one column accepts in a where clause: a bare value means equals (a list means in, or has
101
+ * all for a list column). The checks are wrapped in [ ] so a choice column ('open' | 'booked')
102
+ * is taken whole: `{ in: ['open', 'booked'] }` is one filter, not one per choice.
103
+ */
104
+ export type ColumnFilter<V> = [
105
+ V
106
+ ] extends [Array<infer E>] ? E | E[] | ListOps<E> : [V] extends [number] ? V | V[] | null | NumberOps : [V] extends [boolean] ? V | null | BoolOps : [V] extends [string] ? V | V[] | null | TextOps<V> : unknown;
107
+ /** A where clause: a filter per column, combined with and, or and not. */
88
108
  export type Where<T> = {
89
109
  [C in keyof T]?: ColumnFilter<NonNullable<T[C]>>;
90
110
  } & {
@@ -96,7 +116,9 @@ export type Where<T> = {
96
116
  export type SortableColumn<T> = Extract<{
97
117
  [C in keyof T]: NonNullable<T[C]> extends unknown[] ? never : C;
98
118
  }[keyof T], string>;
119
+ /** How a query picks, orders, pages and narrows rows. */
99
120
  export interface QueryOptions<T, S extends keyof T = keyof T> {
121
+ /** Which rows: a filter per column. */
100
122
  where?: Where<T>;
101
123
  /** One sort column; id breaks ties. Default: created_at, newest first. */
102
124
  orderBy?: [SortableColumn<T>, 'asc' | 'desc'];
@@ -104,10 +126,12 @@ export interface QueryOptions<T, S extends keyof T = keyof T> {
104
126
  limit?: number;
105
127
  /** The previous page's next. Repeat the same orderBy. */
106
128
  after?: string;
129
+ /** Only these columns. Default: every column this caller may read. */
107
130
  select?: S[];
108
131
  /** IANA zone for dates and time variables. Default Australia/Sydney. */
109
132
  tz?: string;
110
133
  }
134
+ /** What the hub assumed for a query (order, limit, time zone, what each time variable meant), so it can be said back. */
111
135
  export interface Applied {
112
136
  order_by: [string, 'asc' | 'desc'];
113
137
  limit: number;
@@ -115,33 +139,58 @@ export interface Applied {
115
139
  time_variables: Record<string, string>;
116
140
  defaults: string[];
117
141
  }
142
+ /** One page of rows. Pass `next` as `after` for the following page. */
118
143
  export interface Page<R> {
144
+ /** The rows on this page. */
119
145
  rows: R[];
146
+ /** Where the next page starts, or null on the last page. */
120
147
  next: string | null;
148
+ /** Rows on this page. */
121
149
  count: number;
122
150
  /** What the hub assumed; repeat it to whoever asked. */
123
151
  applied: Applied;
152
+ /** Columns left out: not marked readable by AI (to the account's token), or sensitive. */
124
153
  hidden: {
125
154
  not_readable_by_ai: number;
126
155
  sensitive: number;
127
156
  };
157
+ /** When the hub read the rows. */
128
158
  asAt: string;
129
159
  }
130
160
  declare const ERROR_CODES: readonly ["unauthenticated", "forbidden", "not_found", "validation", "consent_blocked", "rate_limited", "conflict", "unavailable"];
161
+ /**
162
+ * Why a call failed, one of a fixed list: unauthenticated, forbidden, not_found, validation,
163
+ * consent_blocked, rate_limited, conflict, unavailable. The hub's own detail is in serverCode.
164
+ */
131
165
  export type ErrorCode = (typeof ERROR_CODES)[number];
166
+ /**
167
+ * Every failure from the hub. `message` is for you, the developer; `personMessage`, when the hub
168
+ * sent one, is the sentence to show the person using your app.
169
+ */
132
170
  export declare class AwesomateError extends Error {
171
+ /** Why it failed. */
133
172
  readonly code: ErrorCode;
173
+ /** The HTTP status, or 0 when the SDK refused before sending. */
134
174
  readonly status: number;
135
175
  /** The column or parameter a validation error is about. */
136
176
  readonly field?: string | undefined;
137
177
  /** The hub's own code, unmapped. */
138
178
  readonly serverCode?: string | undefined;
139
- constructor(code: ErrorCode, message: string, status: number,
179
+ /** A sentence for the person using the app, when the hub sent one (a refused voice call: "You've used today's voice time"). */
180
+ readonly personMessage?: string | undefined;
181
+ constructor(
182
+ /** Why it failed. */
183
+ code: ErrorCode, message: string,
184
+ /** The HTTP status, or 0 when the SDK refused before sending. */
185
+ status: number,
140
186
  /** The column or parameter a validation error is about. */
141
187
  field?: string | undefined,
142
188
  /** The hub's own code, unmapped. */
143
- serverCode?: string | undefined);
189
+ serverCode?: string | undefined,
190
+ /** A sentence for the person using the app, when the hub sent one (a refused voice call: "You've used today's voice time"). */
191
+ personMessage?: string | undefined);
144
192
  }
193
+ /** Options for createClient(). */
145
194
  export interface ClientOptions {
146
195
  /**
147
196
  * The account's hosting token (amt_pat_...) with crm:read, or an app's server key (ak_...) from
@@ -149,9 +198,15 @@ export interface ClientOptions {
149
198
  * kinds it was made for; changing kinds, queries or recipes needs the account's token.
150
199
  */
151
200
  token: string;
201
+ /** Default https://hub.awesomate.ai. */
152
202
  baseUrl?: string;
203
+ /** Your own fetch. Default: the global one (Node 18 and later). */
153
204
  fetch?: typeof fetch;
154
205
  }
206
+ /**
207
+ * The server client, from createClient(): reads and writes the account's data with its token or
208
+ * an app's server key. Keep it on a server; it never works in a browser.
209
+ */
155
210
  export declare class AwesomateClient {
156
211
  private readonly opts;
157
212
  private readonly base;
@@ -179,6 +234,16 @@ export declare class AwesomateClient {
179
234
  content: string;
180
235
  as_at: string;
181
236
  }>;
237
+ /**
238
+ * Rows of one kind that this token can read, a page at a time.
239
+ *
240
+ * @example
241
+ * const { rows, next } = await db.query('contact', {
242
+ * where: { created_at: { gte: '$YEAR_BEGIN' } },
243
+ * orderBy: ['created_at', 'desc'],
244
+ * limit: 50,
245
+ * });
246
+ */
182
247
  query<K extends KindName, S extends keyof RowOf<K> = keyof RowOf<K>>(kind: K, options?: QueryOptions<RowOf<K>, S>): Promise<Page<Pick<RowOf<K>, S>>>;
183
248
  /** Every matching row, a page at a time. maxRows guards against reading a whole list by accident. */
184
249
  queryAll<K extends KindName, S extends keyof RowOf<K> = keyof RowOf<K>>(kind: K, options?: Omit<QueryOptions<RowOf<K>, S>, 'after'> & {
@@ -204,6 +269,7 @@ export declare class AwesomateClient {
204
269
  * stored, so a column it cannot read is refused now rather than when it runs.
205
270
  */
206
271
  saveQuery(spec: SavedQuerySpec): Promise<SavedQueryDescription>;
272
+ /** Archive a saved query by its key. */
207
273
  archiveQuery(key: string): Promise<void>;
208
274
  /** Run a saved query with its params. An optional param left out drops its condition. */
209
275
  run<Q extends QueryName>(query: Q, params?: ParamsOf<Q>, options?: {
@@ -218,6 +284,7 @@ export declare class AwesomateClient {
218
284
  * {"$param": "<name>"} takes a caller's value, {"$step": "<as>"} the id an earlier step wrote.
219
285
  */
220
286
  saveRecipe(spec: RecipeSpec): Promise<RecipeDescription>;
287
+ /** Archive a write recipe by its key. */
221
288
  archiveRecipe(key: string): Promise<void>;
222
289
  /**
223
290
  * Run a write recipe. Every step commits or none does; a refusal names the step and the field.
@@ -231,6 +298,13 @@ export declare class AwesomateClient {
231
298
  * token (hosting:read, every plan), never an app key.
232
299
  */
233
300
  business(): Promise<BusinessIdentity>;
301
+ /**
302
+ * The business map: the seven divisions every business has, the jobs in each and who holds them,
303
+ * which agents and automations help which job and how far each may go, and what is missing, most
304
+ * important first. Read only: the owner changes the map in the hub. Needs the account's token
305
+ * (hosting:read, every plan), never an app key, and the account must have the business map.
306
+ */
307
+ businessMap(): Promise<BusinessMap>;
234
308
  /** Details waiting for the owner's yes on Your business: from the website, the account, a team member or a file. */
235
309
  businessSuggestions(): Promise<BusinessSuggestion[]>;
236
310
  /** Grouped numbers (counts, sums) in the Business Data API's shape. */
@@ -270,9 +344,13 @@ export interface BusinessFact {
270
344
  /** Where it came from, in words: "confirmed by the owner", "added by a team member", "website research"... */
271
345
  source: string;
272
346
  }
347
+ /** The business, as business() returns it. */
273
348
  export interface BusinessIdentity {
349
+ /** The account's slug. */
274
350
  account: string;
351
+ /** The business's name, when known. */
275
352
  name: string | null;
353
+ /** The details, grouped for reading. */
276
354
  groups: Array<{
277
355
  id: string;
278
356
  title: string;
@@ -286,8 +364,133 @@ export interface BusinessIdentity {
286
364
  };
287
365
  /** Whether each source could be read: 'read', 'empty' or 'unavailable'. */
288
366
  sources: Record<'confirmed_details' | 'saved_details' | 'account_details' | 'website_research', string>;
367
+ /** Where the owner changes these details, in words. */
289
368
  how_to_change: string;
290
369
  }
370
+ /** How far someone, or an agent, decides alone: 1 find out, 2 suggest options, 3 recommend and wait, 4 do it then tell, 5 report only the exceptions. */
371
+ export type BusinessMapLevel = 1 | 2 | 3 | 4 | 5;
372
+ /** Something that helps: an agent, a library install, an automation Awesomate built, or a tool outside Awesomate. */
373
+ export interface BusinessMapHelper {
374
+ kind: 'agent' | 'bundle' | 'template' | 'build' | 'external';
375
+ ref: string;
376
+ label: string;
377
+ }
378
+ /** A job on the map. `slug` is its stable name for routing work: "whoever holds quotes". */
379
+ export interface BusinessMapJob {
380
+ id: number;
381
+ slug: string;
382
+ title: string;
383
+ mission: string | null;
384
+ /** active: someone holds it. open: nobody does. hire_next: the owner's next hire. */
385
+ state: 'active' | 'open' | 'hire_next';
386
+ isOwnerJob: boolean;
387
+ headsDivision: boolean;
388
+ departmentNo: number | null;
389
+ reportsTo: {
390
+ id: number;
391
+ title: string;
392
+ } | null;
393
+ holders: Array<{
394
+ membershipId: number;
395
+ name: string;
396
+ accountable: boolean;
397
+ timeSharePct: number | null;
398
+ }>;
399
+ responsibilities: Array<{
400
+ text: string;
401
+ level: BusinessMapLevel;
402
+ exceptions: string | null;
403
+ }>;
404
+ helpers: Array<BusinessMapHelper & {
405
+ id: number;
406
+ supervisor: string;
407
+ level: BusinessMapLevel | null;
408
+ exceptions: string | null;
409
+ minutesSavedPerRun: number | null;
410
+ }>;
411
+ }
412
+ /** One of the seven divisions, in board order (7 first). */
413
+ export interface BusinessMapDivision {
414
+ no: 1 | 2 | 3 | 4 | 5 | 6 | 7;
415
+ /** Envision, Form, Promise, Balance, Fulfil, Refine, Share. */
416
+ verb: string;
417
+ name: string;
418
+ purpose: string;
419
+ stage: 'survive' | 'grow' | 'scale';
420
+ /** Who runs it. byDefault: nobody else has been given it, so the owner does. */
421
+ runBy: {
422
+ name: string;
423
+ byDefault: boolean;
424
+ };
425
+ jobs: BusinessMapJob[];
426
+ /** Helping here but not yet put on a job, with how they were placed, in words. */
427
+ helpers: Array<BusinessMapHelper & {
428
+ placedBy: string;
429
+ }>;
430
+ departments: Array<{
431
+ no: number;
432
+ name: string;
433
+ gloss: string;
434
+ }>;
435
+ ideas: Array<{
436
+ label: string;
437
+ what: string;
438
+ status: 'live' | 'library' | 'coming' | 'building' | 'planned';
439
+ path?: string;
440
+ }>;
441
+ numbers: Array<{
442
+ label: string;
443
+ kind: 'lead' | 'result';
444
+ }>;
445
+ question: string;
446
+ }
447
+ /** The business map, as businessMap() returns it. No email addresses. */
448
+ export interface BusinessMap {
449
+ business: {
450
+ name: string | null;
451
+ ownerName: string;
452
+ };
453
+ /** false: the owner has not started the map, so it is worked out from what the account runs. */
454
+ stored: boolean;
455
+ people: Array<{
456
+ id: number | null;
457
+ name: string;
458
+ kind: 'owner' | 'staff' | 'contractor' | 'adviser' | null;
459
+ role: 'owner' | 'full' | 'view' | null;
460
+ location: string | null;
461
+ fromTeamAccess: boolean;
462
+ }>;
463
+ divisions: BusinessMapDivision[];
464
+ /** Helpers we could not place on a division. */
465
+ unplaced: Array<BusinessMapHelper & {
466
+ placedBy: string;
467
+ }>;
468
+ /** What is missing, most important first. */
469
+ gaps: Array<{
470
+ kind: 'no_helper' | 'job_open' | 'owner_everywhere' | 'unplaced';
471
+ division: number | null;
472
+ title: string;
473
+ detail: string;
474
+ action: {
475
+ label: string;
476
+ path: string;
477
+ } | null;
478
+ }>;
479
+ settings: {
480
+ adviser: string | null;
481
+ runsWeek: string | null;
482
+ };
483
+ counts: {
484
+ people: number;
485
+ helpers: number;
486
+ placed: number;
487
+ divisionsWithHelpers: number;
488
+ jobs: number;
489
+ };
490
+ /** Sources that could not be read just now: a missing helper may simply not have been read. */
491
+ unavailable: string[];
492
+ }
493
+ /** A detail waiting for the owner's yes. */
291
494
  export interface BusinessSuggestion {
292
495
  id: number;
293
496
  key: string;
@@ -297,6 +500,7 @@ export interface BusinessSuggestion {
297
500
  sourceRef: string | null;
298
501
  recordedAt: string;
299
502
  }
503
+ /** One attribute (column) of a kind. */
300
504
  export interface AttributeSpec {
301
505
  key: string;
302
506
  label?: string;
@@ -307,6 +511,7 @@ export interface AttributeSpec {
307
511
  /** Default false. Only readable attributes reach query(), get() and the generated types. */
308
512
  readable_by_ai?: boolean;
309
513
  }
514
+ /** A kind to define: a table an app keeps. */
310
515
  export interface KindSpec {
311
516
  key: string;
312
517
  label?: string;
@@ -321,6 +526,7 @@ export interface KindSpec {
321
526
  required?: boolean;
322
527
  }>;
323
528
  }
529
+ /** A kind as the account has defined it. */
324
530
  export interface KindDescription extends Omit<KindSpec, 'attributes'> {
325
531
  storage: 'plain' | 'tracked';
326
532
  attributes: Array<Required<Pick<AttributeSpec, 'key' | 'label' | 'type' | 'required' | 'sensitivity' | 'readable_by_ai'>> & {
@@ -331,13 +537,16 @@ export interface KindDescription extends Omit<KindSpec, 'attributes'> {
331
537
  for_agents: string;
332
538
  };
333
539
  }
540
+ /** Options for write(). */
334
541
  export interface WriteOptions {
335
542
  /** Change this record instead of creating one; values not given stay. */
336
543
  id?: string;
337
544
  /** Set a link by its key ('<id>'), or end it (null). */
338
545
  links?: Record<string, string | null>;
339
546
  }
547
+ /** The type of a saved query's or recipe's parameter. */
340
548
  export type ParamType = 'text' | 'number' | 'date' | 'datetime' | 'boolean' | 'uuid' | 'text_list' | 'number_list';
549
+ /** One parameter of a saved query or recipe. */
341
550
  export interface ParamSpec {
342
551
  name: string;
343
552
  /** Default text. */
@@ -350,6 +559,7 @@ export interface ParamSpec {
350
559
  export type Param = {
351
560
  $param: string;
352
561
  };
562
+ /** A saved query to store with saveQuery(). */
353
563
  export interface SavedQuerySpec {
354
564
  key: string;
355
565
  label: string;
@@ -364,11 +574,13 @@ export interface SavedQuerySpec {
364
574
  };
365
575
  params?: ParamSpec[];
366
576
  }
577
+ /** A saved query as stored. */
367
578
  export interface SavedQueryDescription extends Required<Omit<SavedQuerySpec, 'description' | 'params'>> {
368
579
  description: string | null;
369
580
  params: ParamSpec[];
370
581
  updated_at: string;
371
582
  }
583
+ /** One step of a write recipe: write a record, or archive one. */
372
584
  export type RecipeStep = {
373
585
  op: 'write_record';
374
586
  kind: string;
@@ -385,6 +597,7 @@ export type RecipeStep = {
385
597
  $step: string;
386
598
  };
387
599
  };
600
+ /** A write recipe to store with saveRecipe(). */
388
601
  export interface RecipeSpec {
389
602
  key: string;
390
603
  label: string;
@@ -392,11 +605,13 @@ export interface RecipeSpec {
392
605
  params?: ParamSpec[];
393
606
  steps: RecipeStep[];
394
607
  }
608
+ /** A write recipe as stored. */
395
609
  export interface RecipeDescription extends Required<Omit<RecipeSpec, 'description' | 'params'>> {
396
610
  description: string | null;
397
611
  params: ParamSpec[];
398
612
  updated_at: string;
399
613
  }
614
+ /** What a recipe run wrote. */
400
615
  export interface RecipeResult {
401
616
  /** The id each step named with `as` wrote. */
402
617
  ids: Record<string, string>;
@@ -405,15 +620,22 @@ export interface RecipeResult {
405
620
  }
406
621
  /** Where a session is kept: localStorage, Capacitor Preferences, or anything with these three. */
407
622
  export interface SessionStorageLike {
623
+ /** The stored value, or null. */
408
624
  getItem(key: string): string | null | Promise<string | null>;
625
+ /** Store a value. */
409
626
  setItem(key: string, value: string): void | Promise<void>;
627
+ /** Forget a value. */
410
628
  removeItem(key: string): void | Promise<void>;
411
629
  }
630
+ /** The signed-in person. */
412
631
  export interface AppUser {
632
+ /** Their app user id. */
413
633
  id: string;
634
+ /** The address they sign in with. */
414
635
  email: string;
415
636
  /** owner, staff, member or one of the account's own; the database re-reads it on every call */
416
637
  role: string;
638
+ /** Their record in the account's Contacts, when linked. */
417
639
  contact_id: string | null;
418
640
  }
419
641
  /** A customer as lookupCustomers() returns them. A field the owner marked sensitive is null. */
@@ -423,10 +645,13 @@ export interface Customer {
423
645
  email: string | null;
424
646
  phone: string | null;
425
647
  }
648
+ /** Options for createAppClient(). */
426
649
  export interface AppClientOptions {
427
650
  /** The app's publishable key (pk_...). Not a secret: it belongs in browser code. */
428
651
  publishableKey: string;
652
+ /** Default https://hub.awesomate.ai. */
429
653
  baseUrl?: string;
654
+ /** Your own fetch. Default: the global one. */
430
655
  fetch?: typeof fetch;
431
656
  /** Default: localStorage in a browser, memory elsewhere. In Capacitor, pass Preferences. */
432
657
  storage?: SessionStorageLike;
@@ -444,20 +669,28 @@ export interface AppClientOptions {
444
669
  /** The part of the WebSocket API live() uses: the browser's, Node 22's, or the ws package's. */
445
670
  export interface WebSocketLike {
446
671
  new (url: string): {
672
+ /** As the browser's WebSocket. */
447
673
  readyState: number;
674
+ /** As the browser's WebSocket. */
448
675
  send(data: string): void;
676
+ /** As the browser's WebSocket. */
449
677
  close(code?: number, reason?: string): void;
678
+ /** As the browser's WebSocket. */
450
679
  onopen: ((ev: unknown) => void) | null;
680
+ /** As the browser's WebSocket. */
451
681
  onmessage: ((ev: {
452
682
  data: unknown;
453
683
  }) => void) | null;
684
+ /** As the browser's WebSocket. */
454
685
  onclose: ((ev: {
455
686
  code: number;
456
687
  reason: string;
457
688
  }) => void) | null;
689
+ /** As the browser's WebSocket. */
458
690
  onerror: ((ev: unknown) => void) | null;
459
691
  };
460
692
  }
693
+ /** What changed since onRows was last called. */
461
694
  export interface LiveChange<R> {
462
695
  /** Rows that are new or changed since the last call. */
463
696
  upserts: R[];
@@ -475,6 +708,7 @@ export interface HerePerson {
475
708
  /** The app's AI assistant, writing a reply. */
476
709
  assistant?: true;
477
710
  }
711
+ /** From here(): say this person is typing, or close the record. */
478
712
  export interface HereHandle {
479
713
  /**
480
714
  * Say this person is typing (call it on each keystroke: it sends at most every few seconds) or has
@@ -484,6 +718,7 @@ export interface HereHandle {
484
718
  /** Close the record: the others stop seeing this person on it. */
485
719
  leave(): void;
486
720
  }
721
+ /** What live() calls. */
487
722
  export interface LiveHandlers<R> {
488
723
  /** Called with the whole list, in the query's order, at first and after every change. */
489
724
  onRows: (rows: R[], change: LiveChange<R> | null) => void;
@@ -492,6 +727,24 @@ export interface LiveHandlers<R> {
492
727
  }
493
728
  /** The sign-in token a link carries, from a URL's fragment (#awesomate_token=...), or null. */
494
729
  export declare function signInTokenFrom(url: string): string | null;
730
+ /** What AwesomateAgent.mount's session() returns: pass it through unchanged. */
731
+ export interface VoiceSession {
732
+ session_id: string;
733
+ url: string;
734
+ token: string;
735
+ expires_at: string;
736
+ max_seconds: number;
737
+ agent_name: string;
738
+ pages: Array<{
739
+ label: string;
740
+ path: string;
741
+ }>;
742
+ say_as: Record<string, string>;
743
+ }
744
+ /**
745
+ * The browser client for one of the account's apps, from createAppClient(): sign-in is under
746
+ * `auth`, and every data call runs as the signed-in person, inside the account's access rules.
747
+ */
495
748
  export declare class AwesomateAppClient {
496
749
  private readonly opts;
497
750
  private readonly base;
@@ -533,6 +786,7 @@ export declare class AwesomateAppClient {
533
786
  onChange: (listener: (user: AppUser | null) => void) => (() => void);
534
787
  /** Called when a sign-in link could not be used (expired, already used, not this app's). */
535
788
  onSignInError: (listener: (err: AwesomateError) => void) => (() => void);
789
+ /** Sign out here and end the session at the hub. */
536
790
  signOut: () => Promise<void>;
537
791
  /** A current access token, for the app's own server to verify against the JWKS. */
538
792
  accessToken: () => Promise<string | null>;
@@ -549,7 +803,15 @@ export declare class AwesomateAppClient {
549
803
  private refresh;
550
804
  /** A data call as the signed-in user: refused once (a disable, a key change), it refreshes and tries again; refused twice, it signs out. */
551
805
  private data;
806
+ /**
807
+ * Rows of one kind that this person may read, a page at a time. The database applies the
808
+ * kind's read rule for their role: a customer gets their own jobs, staff whatever theirs allows.
809
+ *
810
+ * @example
811
+ * const { rows } = await app.query('job', { orderBy: ['created_at', 'desc'], limit: 20 });
812
+ */
552
813
  query<K extends KindName, S extends keyof RowOf<K> = keyof RowOf<K>>(kind: K, options?: QueryOptions<RowOf<K>, S>): Promise<Omit<Page<Pick<RowOf<K>, S>>, 'hidden'>>;
814
+ /** Every matching row this person may read, a page at a time. maxRows (default 10,000) guards against reading a whole list by accident. */
553
815
  queryAll<K extends KindName, S extends keyof RowOf<K> = keyof RowOf<K>>(kind: K, options?: Omit<QueryOptions<RowOf<K>, S>, 'after'> & {
554
816
  maxRows?: number;
555
817
  }): AsyncGenerator<Pick<RowOf<K>, S>>;
@@ -557,6 +819,7 @@ export declare class AwesomateAppClient {
557
819
  get<K extends KindName>(kind: K, id: string): Promise<RowOf<K> | null>;
558
820
  /** Create a record (returns its id) or change one with options.id, inside the kind's write rule for this user. */
559
821
  write<K extends KindName>(kind: K, data: Partial<RowOf<K>> | Record<string, unknown>, options?: WriteOptions): Promise<string>;
822
+ /** Archive a record, inside the kind's write rule for this person. */
560
823
  archive<K extends KindName>(kind: K, id: string): Promise<void>;
561
824
  /**
562
825
  * Run a recipe the account opened to this person's role: "accept this quote", something their
@@ -575,6 +838,19 @@ export declare class AwesomateAppClient {
575
838
  ids?: string[];
576
839
  limit?: number;
577
840
  }): Promise<Customer[]>;
841
+ /**
842
+ * The agent this app talks with by voice, or null when the account has not picked one. Pass it
843
+ * to AwesomateAgent.mount as agentId; the hub decides which agent answers, never the page.
844
+ */
845
+ voiceAgent(): Promise<string | null>;
846
+ /**
847
+ * A voice session with the app's agent, as this signed-in person:
848
+ * `AwesomateAgent.mount({ agentId, session: () => app.voiceSession() })`. The agent greets them
849
+ * by name and, when the app's assistant is set up, knows the recent conversations they can see, and nothing more. A refusal's serverCode says why:
850
+ * no_voice_agent (the app has none), plan_required, consent_required, person_daily, agent_daily;
851
+ * its personMessage is the sentence to show the person, which the AwesomateAgent widget shows.
852
+ */
853
+ voiceSession(): Promise<VoiceSession>;
578
854
  /**
579
855
  * A list kept current: onRows gets the whole list (up to 200 rows, the query's order) at first
580
856
  * and again whenever a row in it is added, changed, removed, or stops being one this person may
@@ -592,6 +868,23 @@ export declare class AwesomateAppClient {
592
868
  */
593
869
  here(recordId: string, onPeople: (people: HerePerson[]) => void, onError?: (err: AwesomateError) => void): HereHandle;
594
870
  }
871
+ /**
872
+ * The browser client for one of the account's apps (Pro and above). Give it the app's publishable
873
+ * key, which is not a secret; never the account's token.
874
+ *
875
+ * @example
876
+ * const app = createAppClient({ publishableKey: 'pk_...' });
877
+ * app.auth.onChange((user) => render(user));
878
+ * await app.auth.signInWithLink(email, { redirectTo: location.href });
879
+ */
595
880
  export declare function createAppClient(options: AppClientOptions): AwesomateAppClient;
881
+ /**
882
+ * The server client: the account's token (amt_pat_... with crm:read) or an app's server key
883
+ * (ak_...). Both are secrets: keep them on a server, never in a browser.
884
+ *
885
+ * @example
886
+ * const db = createClient({ token: process.env.AWESOMATE_TOKEN! });
887
+ * const { rows } = await db.query('contact', { limit: 10 });
888
+ */
596
889
  export declare function createClient(options: ClientOptions): AwesomateClient;
597
890
  export {};
package/dist/index.js CHANGED
@@ -18,28 +18,42 @@
18
18
  * const stop = app.live('job', {}, (rows) => render(rows)); // and kept current as they change
19
19
  *
20
20
  * The account's token (a hosting PAT with crm:read) is a secret: use it on a server, never in a
21
- * browser. End-user sign-in for browser apps comes in a later version. Reading and running saved
22
- * queries works on every plan; writing app data (kinds, records, saved queries, recipes) needs
23
- * crm:write, Support Plus and above.
21
+ * browser. Reading and running saved queries works on every plan; writing app data (kinds,
22
+ * records, saved queries, recipes) needs crm:write, Support Plus and above.
23
+ *
24
+ * Docs: https://hub.awesomate.ai/docs/sdk/
24
25
  */
25
- export const VERSION = '0.7.0';
26
+ /** This package's version, sent to the hub with every server call. */
27
+ export const VERSION = '0.14.0';
26
28
  const DEFAULT_BASE = 'https://hub.awesomate.ai';
27
29
  const ERROR_CODES = ['unauthenticated', 'forbidden', 'not_found', 'validation', 'consent_blocked', 'rate_limited', 'conflict', 'unavailable'];
30
+ /**
31
+ * Every failure from the hub. `message` is for you, the developer; `personMessage`, when the hub
32
+ * sent one, is the sentence to show the person using your app.
33
+ */
28
34
  export class AwesomateError extends Error {
29
35
  code;
30
36
  status;
31
37
  field;
32
38
  serverCode;
33
- constructor(code, message, status,
39
+ personMessage;
40
+ constructor(
41
+ /** Why it failed. */
42
+ code, message,
43
+ /** The HTTP status, or 0 when the SDK refused before sending. */
44
+ status,
34
45
  /** The column or parameter a validation error is about. */
35
46
  field,
36
47
  /** The hub's own code, unmapped. */
37
- serverCode) {
48
+ serverCode,
49
+ /** A sentence for the person using the app, when the hub sent one (a refused voice call: "You've used today's voice time"). */
50
+ personMessage) {
38
51
  super(message);
39
52
  this.code = code;
40
53
  this.status = status;
41
54
  this.field = field;
42
55
  this.serverCode = serverCode;
56
+ this.personMessage = personMessage;
43
57
  this.name = 'AwesomateError';
44
58
  }
45
59
  }
@@ -70,6 +84,10 @@ function keyPath(method, path) {
70
84
  return `/api/sdk/v1/server${rest}`;
71
85
  throw new AwesomateError('forbidden', 'An app key reads and writes records and runs saved queries and recipes. This needs the account\'s token.', 0);
72
86
  }
87
+ /**
88
+ * The server client, from createClient(): reads and writes the account's data with its token or
89
+ * an app's server key. Keep it on a server; it never works in a browser.
90
+ */
73
91
  export class AwesomateClient {
74
92
  opts;
75
93
  base;
@@ -123,6 +141,16 @@ export class AwesomateClient {
123
141
  types() {
124
142
  return this.request('GET', '/api/my-crm/v1/rows/types');
125
143
  }
144
+ /**
145
+ * Rows of one kind that this token can read, a page at a time.
146
+ *
147
+ * @example
148
+ * const { rows, next } = await db.query('contact', {
149
+ * where: { created_at: { gte: '$YEAR_BEGIN' } },
150
+ * orderBy: ['created_at', 'desc'],
151
+ * limit: 50,
152
+ * });
153
+ */
126
154
  async query(kind, options = {}) {
127
155
  const r = await this.request('POST', '/api/my-crm/v1/rows/query', { kind, ...options });
128
156
  return { rows: r.rows, next: r.next, count: r.count, applied: r.applied, hidden: r.hidden, asAt: r.as_at };
@@ -186,6 +214,7 @@ export class AwesomateClient {
186
214
  async saveQuery(spec) {
187
215
  return (await this.request('POST', '/api/my-crm/v1/queries', spec, false)).query;
188
216
  }
217
+ /** Archive a saved query by its key. */
189
218
  async archiveQuery(key) {
190
219
  await this.request('DELETE', `/api/my-crm/v1/queries/${encodeURIComponent(key)}`, undefined, false);
191
220
  }
@@ -205,6 +234,7 @@ export class AwesomateClient {
205
234
  async saveRecipe(spec) {
206
235
  return (await this.request('POST', '/api/my-crm/v1/recipes', spec, false)).recipe;
207
236
  }
237
+ /** Archive a write recipe by its key. */
208
238
  async archiveRecipe(key) {
209
239
  await this.request('DELETE', `/api/my-crm/v1/recipes/${encodeURIComponent(key)}`, undefined, false);
210
240
  }
@@ -224,6 +254,15 @@ export class AwesomateClient {
224
254
  business() {
225
255
  return this.request('GET', '/api/my-business/v1/identity?format=json');
226
256
  }
257
+ /**
258
+ * The business map: the seven divisions every business has, the jobs in each and who holds them,
259
+ * which agents and automations help which job and how far each may go, and what is missing, most
260
+ * important first. Read only: the owner changes the map in the hub. Needs the account's token
261
+ * (hosting:read, every plan), never an app key, and the account must have the business map.
262
+ */
263
+ async businessMap() {
264
+ return (await this.request('GET', '/api/my-business/v1/map?format=json')).map;
265
+ }
227
266
  /** Details waiting for the owner's yes on Your business: from the website, the account, a team member or a file. */
228
267
  async businessSuggestions() {
229
268
  return (await this.request('GET', '/api/my-business/v1/proposals')).proposals;
@@ -254,6 +293,10 @@ export function signInTokenFrom(url) {
254
293
  return v && v.length >= 20 ? v : null;
255
294
  }
256
295
  const REFRESH_EARLY_MS = 30_000;
296
+ /**
297
+ * The browser client for one of the account's apps, from createAppClient(): sign-in is under
298
+ * `auth`, and every data call runs as the signed-in person, inside the account's access rules.
299
+ */
257
300
  export class AwesomateAppClient {
258
301
  opts;
259
302
  base;
@@ -351,6 +394,7 @@ export class AwesomateAppClient {
351
394
  this.linkErrorListeners.add(listener);
352
395
  return () => { this.linkErrorListeners.delete(listener); };
353
396
  },
397
+ /** Sign out here and end the session at the hub. */
354
398
  signOut: async () => {
355
399
  const s = await this.load();
356
400
  await this.clear();
@@ -382,7 +426,7 @@ export class AwesomateAppClient {
382
426
  return json;
383
427
  const server = typeof json.code === 'string' ? json.code : undefined;
384
428
  const message = typeof json.error === 'string' ? json.error : `The hub answered ${res.status}.`;
385
- throw new AwesomateError(errorCode(res.status, server), message, res.status, typeof json.field === 'string' ? json.field : undefined, server);
429
+ throw new AwesomateError(errorCode(res.status, server), message, res.status, typeof json.field === 'string' ? json.field : undefined, server, typeof json.message === 'string' ? json.message : undefined);
386
430
  }
387
431
  async read() {
388
432
  try {
@@ -488,10 +532,18 @@ export class AwesomateAppClient {
488
532
  return this.request(method, path, body, s.access_token);
489
533
  }
490
534
  }
535
+ /**
536
+ * Rows of one kind that this person may read, a page at a time. The database applies the
537
+ * kind's read rule for their role: a customer gets their own jobs, staff whatever theirs allows.
538
+ *
539
+ * @example
540
+ * const { rows } = await app.query('job', { orderBy: ['created_at', 'desc'], limit: 20 });
541
+ */
491
542
  async query(kind, options = {}) {
492
543
  const r = await this.data('POST', '/rows/query', { kind, ...options });
493
544
  return { rows: r.rows, next: r.next, count: r.count, applied: r.applied, asAt: r.as_at };
494
545
  }
546
+ /** Every matching row this person may read, a page at a time. maxRows (default 10,000) guards against reading a whole list by accident. */
495
547
  async *queryAll(kind, options = {}) {
496
548
  const { maxRows = 10_000, ...rest } = options;
497
549
  let after;
@@ -524,6 +576,7 @@ export class AwesomateAppClient {
524
576
  });
525
577
  return r.result.id;
526
578
  }
579
+ /** Archive a record, inside the kind's write rule for this person. */
527
580
  async archive(kind, id) {
528
581
  await this.data('POST', '/call', { fn: 'archive_record', args: { kind, id } });
529
582
  }
@@ -544,6 +597,23 @@ export class AwesomateAppClient {
544
597
  async lookupCustomers(options) {
545
598
  return (await this.data('POST', '/customers/lookup', options)).customers;
546
599
  }
600
+ /**
601
+ * The agent this app talks with by voice, or null when the account has not picked one. Pass it
602
+ * to AwesomateAgent.mount as agentId; the hub decides which agent answers, never the page.
603
+ */
604
+ async voiceAgent() {
605
+ return (await this.data('GET', '/me')).app.voice_agent_id ?? null;
606
+ }
607
+ /**
608
+ * A voice session with the app's agent, as this signed-in person:
609
+ * `AwesomateAgent.mount({ agentId, session: () => app.voiceSession() })`. The agent greets them
610
+ * by name and, when the app's assistant is set up, knows the recent conversations they can see, and nothing more. A refusal's serverCode says why:
611
+ * no_voice_agent (the app has none), plan_required, consent_required, person_daily, agent_daily;
612
+ * its personMessage is the sentence to show the person, which the AwesomateAgent widget shows.
613
+ */
614
+ async voiceSession() {
615
+ return this.data('POST', '/agents/sessions');
616
+ }
547
617
  /**
548
618
  * A list kept current: onRows gets the whole list (up to 200 rows, the query's order) at first
549
619
  * and again whenever a row in it is added, changed, removed, or stops being one this person may
@@ -855,9 +925,26 @@ class LiveSocket {
855
925
  this.failAll(new AwesomateError('unauthenticated', 'Signed out.', 401));
856
926
  }
857
927
  }
928
+ /**
929
+ * The browser client for one of the account's apps (Pro and above). Give it the app's publishable
930
+ * key, which is not a secret; never the account's token.
931
+ *
932
+ * @example
933
+ * const app = createAppClient({ publishableKey: 'pk_...' });
934
+ * app.auth.onChange((user) => render(user));
935
+ * await app.auth.signInWithLink(email, { redirectTo: location.href });
936
+ */
858
937
  export function createAppClient(options) {
859
938
  return new AwesomateAppClient(options);
860
939
  }
940
+ /**
941
+ * The server client: the account's token (amt_pat_... with crm:read) or an app's server key
942
+ * (ak_...). Both are secrets: keep them on a server, never in a browser.
943
+ *
944
+ * @example
945
+ * const db = createClient({ token: process.env.AWESOMATE_TOKEN! });
946
+ * const { rows } = await db.query('contact', { limit: 10 });
947
+ */
861
948
  export function createClient(options) {
862
949
  return new AwesomateClient(options);
863
950
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/sdk",
3
- "version": "0.12.0",
3
+ "version": "0.14.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",
@@ -28,11 +28,14 @@
28
28
  "scripts": {
29
29
  "build": "tsc -p tsconfig.json",
30
30
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p test/typing/tsconfig.json",
31
- "test": "npm run build && node --test test/*.test.mjs && tsc -p test/typing/tsconfig.json",
31
+ "test": "npm run build && node --test test/*.test.mjs && tsc -p test/typing/tsconfig.json && npm run docs:check",
32
+ "docs": "node scripts/build-docs.mjs",
33
+ "docs:check": "node scripts/build-docs.mjs --check",
32
34
  "prepublishOnly": "npm run typecheck && npm test"
33
35
  },
34
36
  "devDependencies": {
35
37
  "@types/node": "^20.17.0",
38
+ "typedoc": "^0.28.20",
36
39
  "typescript": "^5.7.2"
37
40
  }
38
41
  }