@awesomate/sdk 0.11.0 → 0.13.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,203 +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
9
  ```
10
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
- ```
32
-
33
- ## Writing app data (Support Plus and above)
34
-
35
- Define the tables an app keeps, then write to them. There's no migration and no SQL; every value is
36
- checked against the kind (types, choices, required attributes and links).
37
-
38
- ```ts
39
- await db.defineKind({
40
- key: 'job', label: 'Job', label_plural: 'Jobs',
41
- attributes: [
42
- { key: 'title', type: 'text', required: true, readable_by_ai: true },
43
- { key: 'status', type: 'choice', choices: ['open', 'booked', 'done'], readable_by_ai: true },
44
- ],
45
- links: [{ key: 'customer', to: 'contact' }],
46
- });
47
- const id = await db.write('job', { title: 'Possum in the roof', status: 'open' }, { links: { customer: contactId } });
48
- await db.write('job', { status: 'booked' }, { id }); // values not given stay
49
- const jobs = await db.query('job', { where: { customer_id: contactId } });
50
- await db.archive('job', id);
51
- ```
52
-
53
- Only attributes marked `readable_by_ai` come back from `query` and `get`; a `sensitive` attribute
54
- can never be readable. Contacts themselves are not written this way: the contact list keeps its
55
- own consent rules.
56
-
57
- ## What you can read
58
-
59
- The built-in contact details (name, email, phone, company, tags, when they were added) and only
60
- the fields you have marked **readable by AI** under Contacts, Your fields in the hub. A field
61
- marked sensitive is never readable. `db.schema()` lists the columns and says how many are hidden.
62
-
63
- ## The where grammar
64
-
65
- `{ column: value }` means equals; `{ column: { op: value } }` uses an operator: `eq`, `neq`, `gt`,
66
- `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `startsWith`, `like`, `ilike`, `isNull`, and for lists
67
- (tags) `has`, `hasAny`, `hasAll`, `isEmpty`. Combine with `and: [...]`, `or: [...]`, `not: {...}`.
68
- A date (`'2026-09-30'`) against a timestamp means that whole day in your time zone; time
69
- variables `$TODAY`, `$WEEK_BEGIN`, `$MONTH_BEGIN`, `$QUARTER_BEGIN`, `$YEAR_BEGIN` and `$FY_BEGIN`
70
- (1 July) take an offset in their own unit (`$MONTH_BEGIN-1` is the start of last month).
71
-
72
- ## Errors
73
-
74
- Every failure is an `AwesomateError` with a `code`: `unauthenticated`, `forbidden`, `not_found`,
75
- `validation` (with `field`), `consent_blocked`, `rate_limited`, `conflict` or `unavailable`.
76
-
77
- ## Keep the token on a server
78
-
79
- The token is your account's hosting token. Use it in a server, a script or a scheduled job, never
80
- in a browser. For a browser, sign your app's own users in (below).
81
-
82
- ## Your app's server: a key of its own
83
-
84
- An app's own server, or an n8n workflow, should not carry the account's token. Give it a server
85
- key instead: Claude makes one with `awesomate_crm_apps` (`create_key`), read or write, for every
86
- kind or only the ones it names. The key is shown once; put it in the server's environment.
87
-
88
- ```ts
89
- const db = createClient({ token: process.env.AWESOMATE_APP_KEY! }); // ak_...
90
- const { rows } = await db.query('job', { where: { status: 'booked' } });
91
- await db.write('job', { status: 'done' }, { id: rows[0].id });
92
- await db.call('book_job', { customer, title: 'Gutter clean', status: 'booked' });
93
- ```
94
-
95
- A key reads and writes records and runs saved queries and recipes, only for its kinds (a recipe
96
- that writes anything else is refused). It reads every attribute of an app kind, and the contact
97
- list only as Claude does. Writes need Support Plus and above. It cannot change kinds, rules,
98
- queries or recipes, and it never works from a browser. Revoking a key stops it at once; switching
99
- the app off stops all of them. From n8n, send it as `Authorization: Bearer ak_...` to
100
- `https://hub.awesomate.ai/api/sdk/v1/server/rows/query` (and `/call`, `/queries/<key>/run`).
101
-
102
- ## Your app's users sign in (Pro and above)
103
-
104
- Set the app up first (Claude Code: `awesomate_crm_apps`): its name, the origins it runs on, and
105
- whether anyone may sign up or only people you add. That gives you a publishable key, which is not a
106
- secret. Then say who may read and change each kind (`awesomate_crm_kinds`, `set_access`), for
107
- 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)
108
12
 
109
13
  ```ts
110
14
  import { createAppClient } from '@awesomate/sdk';
111
15
 
112
- const app = createAppClient({ publishableKey: 'pk_...' });
113
-
114
- // Render on every change of person: signed in, signed out, a new role.
115
- app.auth.onChange((u) => render(u));
116
- app.auth.onSignInError((err) => showMessage(err.message));
16
+ const app = createAppClient({ publishableKey: 'pk_...' }); // not a secret
117
17
 
118
- // 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
119
20
  await app.auth.signInWithLink(email, { redirectTo: location.href });
120
21
 
121
- // Everywhere else: the same calls as the server client, as this person.
122
- const { rows } = await app.query('job', { orderBy: ['created_at', 'desc'] });
123
- await app.write('forum_post', { body: 'Booked for Tuesday' }, { links: { job: rows[0].id } });
124
- await app.auth.signOut();
125
- ```
126
-
127
- The link signs them in by itself. In a browser the client takes the link from the address bar,
128
- on load or when it arrives in a tab that is already open (only the part after `#` changes, so
129
- the page does not reload), clears it at once and tells `onChange`. A link that has expired or
130
- was already used goes to `onSignInError`. `await app.auth.completeSignIn()` still works and
131
- returns the same sign-in, never a second exchange. Pass `handleSignInLinks: false` to take links
132
- yourself. `onChange` hears about people, not tokens: the routine token refresh is silent.
133
-
134
- ### Lists that keep themselves current
135
-
136
- ```ts
137
- const stop = app.live('job', { orderBy: ['created_at', 'desc'], limit: 50 }, (rows) => render(rows));
138
- // 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 });
139
25
  ```
140
26
 
141
- `live()` calls you with the whole list at first and again whenever a row in it is added, changed,
142
- removed, or stops being one this person may read (a job moved to another customer leaves their
143
- list, with its memos). Changes made anywhere reach the list: in this app, by your team in Claude
144
- Code, by an n8n workflow. One connection serves every list on the client; it reconnects by itself
145
- and takes a fresh copy when it does. Up to 200 rows a list and 20 lists a connection. Pass
146
- `{ onRows, onError }` to hear about a list the server refused (a column that does not exist).
147
- Outside a browser, give it a WebSocket: `createAppClient({ publishableKey, WebSocket })`.
148
-
149
- While a list is open in a browser, the connection also tells the hub whether the tab is in front.
150
- If the account switched on reply emails, someone looking at the app isn't emailed about a reply,
151
- but a tab left open in the background doesn't count as looking, and closing the last one counts as
152
- leaving straight away.
153
-
154
- A conversation can also show who else has it open and who is typing:
27
+ ## On a server: your account's data
155
28
 
156
29
  ```ts
157
- const room = app.here(job.id, (people) => showWhoIsHere(people)); // [{ user, role, name, typing, assistant? }]
158
- messageBox.oninput = () => room.typing(true); // sent at most every few seconds
159
- messageBox.onblur = () => room.typing(false);
160
- // later: room.leave();
161
- ```
162
-
163
- Everyone else with that record open and their tab in front is listed once, and the app's
164
- assistant appears while it writes a reply. The hub checks this person may read the record, and
165
- keeps checking; a refusal goes to the optional third argument, `onError`. Nothing is stored.
166
-
167
- ### Staff find a customer
30
+ import { createClient } from '@awesomate/sdk';
168
31
 
169
- People in an app read no contacts but their own, so by default staff cannot put a job under a
170
- customer. Switch it on per app for the roles that need it (`awesomate_crm_apps`,
171
- `customer_lookup`, never the role customers sign in with). Those roles can then search by name or
172
- email and point a record at any customer:
32
+ const db = createClient({ token: process.env.AWESOMATE_TOKEN! }); // a secret: never in a browser
173
33
 
174
- ```ts
175
- const [jo] = await app.lookupCustomers({ q: 'jo' }); // or { ids: [...] }, up to 50
176
- 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 } });
177
36
  ```
178
37
 
179
- Name, email and phone only, and nothing the owner marked sensitive. Anyone else is refused.
180
-
181
- An attribute can also be visible to some roles only (`awesomate_crm_kinds`, `set_visibility`, for
182
- example a cost only `staff` see). For everyone else it comes back `null` and a write that sets it
183
- is refused, whatever the app asks for.
184
-
185
- Some things a customer should do without being able to edit the record: accept their quote, say.
186
- Save it as a write recipe and open it to their role (`awesomate_crm_recipes`, `run_by`, Pro and
187
- above), then:
38
+ Types for your own account's kinds, saved queries and recipes:
188
39
 
189
- ```ts
190
- await app.call('accept_quote', { job: job.id });
40
+ ```bash
41
+ npx @awesomate/sdk types --out awesomate.d.ts
191
42
  ```
192
43
 
193
- The database lets them do exactly the recipe's steps: only its params take their values, every
194
- other value is fixed, and an existing record must be one they can already read. A recipe not
195
- opened to their role is "not found".
44
+ ## What changed
196
45
 
197
- The rules are kept in your own database, which applies them to every read and write, so a mistake
198
- in the app cannot show anyone more than their role allows. A disabled person is refused on their
199
- next call. Sessions are kept in `localStorage`; in a Capacitor app pass `storage` (Preferences) and
200
- call `completeSignIn(url)` with the deep link that opened the app. Your own server can verify
201
- `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/).
202
47
 
203
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.13.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,12 +284,22 @@ 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.
224
291
  * Never retried: a write that may have landed is not safe to send twice.
225
292
  */
226
293
  call<R extends RecipeName>(recipe: R, args?: ArgsOf<R>): Promise<RecipeResult>;
294
+ /**
295
+ * The business itself: the details Awesomate keeps about it (name, what it does, voice, colours,
296
+ * how customers reach it), grouped, each with where it came from. `confirmed` details are the
297
+ * ones agents and automations use; `suggestion` ones wait for the owner. Needs the account's
298
+ * token (hosting:read, every plan), never an app key.
299
+ */
300
+ business(): Promise<BusinessIdentity>;
301
+ /** Details waiting for the owner's yes on Your business: from the website, the account, a team member or a file. */
302
+ businessSuggestions(): Promise<BusinessSuggestion[]>;
227
303
  /** Grouped numbers (counts, sums) in the Business Data API's shape. */
228
304
  aggregate(request: {
229
305
  measures: Array<{
@@ -251,6 +327,50 @@ export declare class AwesomateClient {
251
327
  limit?: number;
252
328
  }): Promise<Record<string, unknown>>;
253
329
  }
330
+ /** One detail about the business. */
331
+ export interface BusinessFact {
332
+ key: string;
333
+ label: string;
334
+ value: string;
335
+ /** confirmed: in use by agents and automations. suggestion: waiting for the owner. */
336
+ status: 'confirmed' | 'suggestion';
337
+ /** Where it came from, in words: "confirmed by the owner", "added by a team member", "website research"... */
338
+ source: string;
339
+ }
340
+ /** The business, as business() returns it. */
341
+ export interface BusinessIdentity {
342
+ /** The account's slug. */
343
+ account: string;
344
+ /** The business's name, when known. */
345
+ name: string | null;
346
+ /** The details, grouped for reading. */
347
+ groups: Array<{
348
+ id: string;
349
+ title: string;
350
+ facts: BusinessFact[];
351
+ }>;
352
+ /** The eight core details every business should have, and which are missing. */
353
+ completeness: {
354
+ core_set: number;
355
+ core_total: number;
356
+ missing_core: string[];
357
+ };
358
+ /** Whether each source could be read: 'read', 'empty' or 'unavailable'. */
359
+ sources: Record<'confirmed_details' | 'saved_details' | 'account_details' | 'website_research', string>;
360
+ /** Where the owner changes these details, in words. */
361
+ how_to_change: string;
362
+ }
363
+ /** A detail waiting for the owner's yes. */
364
+ export interface BusinessSuggestion {
365
+ id: number;
366
+ key: string;
367
+ value: string;
368
+ /** website, research, import, team_member, staff, upload, legacy_variables... */
369
+ sourceKind: string;
370
+ sourceRef: string | null;
371
+ recordedAt: string;
372
+ }
373
+ /** One attribute (column) of a kind. */
254
374
  export interface AttributeSpec {
255
375
  key: string;
256
376
  label?: string;
@@ -261,6 +381,7 @@ export interface AttributeSpec {
261
381
  /** Default false. Only readable attributes reach query(), get() and the generated types. */
262
382
  readable_by_ai?: boolean;
263
383
  }
384
+ /** A kind to define: a table an app keeps. */
264
385
  export interface KindSpec {
265
386
  key: string;
266
387
  label?: string;
@@ -275,6 +396,7 @@ export interface KindSpec {
275
396
  required?: boolean;
276
397
  }>;
277
398
  }
399
+ /** A kind as the account has defined it. */
278
400
  export interface KindDescription extends Omit<KindSpec, 'attributes'> {
279
401
  storage: 'plain' | 'tracked';
280
402
  attributes: Array<Required<Pick<AttributeSpec, 'key' | 'label' | 'type' | 'required' | 'sensitivity' | 'readable_by_ai'>> & {
@@ -285,13 +407,16 @@ export interface KindDescription extends Omit<KindSpec, 'attributes'> {
285
407
  for_agents: string;
286
408
  };
287
409
  }
410
+ /** Options for write(). */
288
411
  export interface WriteOptions {
289
412
  /** Change this record instead of creating one; values not given stay. */
290
413
  id?: string;
291
414
  /** Set a link by its key ('<id>'), or end it (null). */
292
415
  links?: Record<string, string | null>;
293
416
  }
417
+ /** The type of a saved query's or recipe's parameter. */
294
418
  export type ParamType = 'text' | 'number' | 'date' | 'datetime' | 'boolean' | 'uuid' | 'text_list' | 'number_list';
419
+ /** One parameter of a saved query or recipe. */
295
420
  export interface ParamSpec {
296
421
  name: string;
297
422
  /** Default text. */
@@ -304,6 +429,7 @@ export interface ParamSpec {
304
429
  export type Param = {
305
430
  $param: string;
306
431
  };
432
+ /** A saved query to store with saveQuery(). */
307
433
  export interface SavedQuerySpec {
308
434
  key: string;
309
435
  label: string;
@@ -318,11 +444,13 @@ export interface SavedQuerySpec {
318
444
  };
319
445
  params?: ParamSpec[];
320
446
  }
447
+ /** A saved query as stored. */
321
448
  export interface SavedQueryDescription extends Required<Omit<SavedQuerySpec, 'description' | 'params'>> {
322
449
  description: string | null;
323
450
  params: ParamSpec[];
324
451
  updated_at: string;
325
452
  }
453
+ /** One step of a write recipe: write a record, or archive one. */
326
454
  export type RecipeStep = {
327
455
  op: 'write_record';
328
456
  kind: string;
@@ -339,6 +467,7 @@ export type RecipeStep = {
339
467
  $step: string;
340
468
  };
341
469
  };
470
+ /** A write recipe to store with saveRecipe(). */
342
471
  export interface RecipeSpec {
343
472
  key: string;
344
473
  label: string;
@@ -346,11 +475,13 @@ export interface RecipeSpec {
346
475
  params?: ParamSpec[];
347
476
  steps: RecipeStep[];
348
477
  }
478
+ /** A write recipe as stored. */
349
479
  export interface RecipeDescription extends Required<Omit<RecipeSpec, 'description' | 'params'>> {
350
480
  description: string | null;
351
481
  params: ParamSpec[];
352
482
  updated_at: string;
353
483
  }
484
+ /** What a recipe run wrote. */
354
485
  export interface RecipeResult {
355
486
  /** The id each step named with `as` wrote. */
356
487
  ids: Record<string, string>;
@@ -359,15 +490,22 @@ export interface RecipeResult {
359
490
  }
360
491
  /** Where a session is kept: localStorage, Capacitor Preferences, or anything with these three. */
361
492
  export interface SessionStorageLike {
493
+ /** The stored value, or null. */
362
494
  getItem(key: string): string | null | Promise<string | null>;
495
+ /** Store a value. */
363
496
  setItem(key: string, value: string): void | Promise<void>;
497
+ /** Forget a value. */
364
498
  removeItem(key: string): void | Promise<void>;
365
499
  }
500
+ /** The signed-in person. */
366
501
  export interface AppUser {
502
+ /** Their app user id. */
367
503
  id: string;
504
+ /** The address they sign in with. */
368
505
  email: string;
369
506
  /** owner, staff, member or one of the account's own; the database re-reads it on every call */
370
507
  role: string;
508
+ /** Their record in the account's Contacts, when linked. */
371
509
  contact_id: string | null;
372
510
  }
373
511
  /** A customer as lookupCustomers() returns them. A field the owner marked sensitive is null. */
@@ -377,10 +515,13 @@ export interface Customer {
377
515
  email: string | null;
378
516
  phone: string | null;
379
517
  }
518
+ /** Options for createAppClient(). */
380
519
  export interface AppClientOptions {
381
520
  /** The app's publishable key (pk_...). Not a secret: it belongs in browser code. */
382
521
  publishableKey: string;
522
+ /** Default https://hub.awesomate.ai. */
383
523
  baseUrl?: string;
524
+ /** Your own fetch. Default: the global one. */
384
525
  fetch?: typeof fetch;
385
526
  /** Default: localStorage in a browser, memory elsewhere. In Capacitor, pass Preferences. */
386
527
  storage?: SessionStorageLike;
@@ -398,20 +539,28 @@ export interface AppClientOptions {
398
539
  /** The part of the WebSocket API live() uses: the browser's, Node 22's, or the ws package's. */
399
540
  export interface WebSocketLike {
400
541
  new (url: string): {
542
+ /** As the browser's WebSocket. */
401
543
  readyState: number;
544
+ /** As the browser's WebSocket. */
402
545
  send(data: string): void;
546
+ /** As the browser's WebSocket. */
403
547
  close(code?: number, reason?: string): void;
548
+ /** As the browser's WebSocket. */
404
549
  onopen: ((ev: unknown) => void) | null;
550
+ /** As the browser's WebSocket. */
405
551
  onmessage: ((ev: {
406
552
  data: unknown;
407
553
  }) => void) | null;
554
+ /** As the browser's WebSocket. */
408
555
  onclose: ((ev: {
409
556
  code: number;
410
557
  reason: string;
411
558
  }) => void) | null;
559
+ /** As the browser's WebSocket. */
412
560
  onerror: ((ev: unknown) => void) | null;
413
561
  };
414
562
  }
563
+ /** What changed since onRows was last called. */
415
564
  export interface LiveChange<R> {
416
565
  /** Rows that are new or changed since the last call. */
417
566
  upserts: R[];
@@ -429,6 +578,7 @@ export interface HerePerson {
429
578
  /** The app's AI assistant, writing a reply. */
430
579
  assistant?: true;
431
580
  }
581
+ /** From here(): say this person is typing, or close the record. */
432
582
  export interface HereHandle {
433
583
  /**
434
584
  * Say this person is typing (call it on each keystroke: it sends at most every few seconds) or has
@@ -438,6 +588,7 @@ export interface HereHandle {
438
588
  /** Close the record: the others stop seeing this person on it. */
439
589
  leave(): void;
440
590
  }
591
+ /** What live() calls. */
441
592
  export interface LiveHandlers<R> {
442
593
  /** Called with the whole list, in the query's order, at first and after every change. */
443
594
  onRows: (rows: R[], change: LiveChange<R> | null) => void;
@@ -446,6 +597,24 @@ export interface LiveHandlers<R> {
446
597
  }
447
598
  /** The sign-in token a link carries, from a URL's fragment (#awesomate_token=...), or null. */
448
599
  export declare function signInTokenFrom(url: string): string | null;
600
+ /** What AwesomateAgent.mount's session() returns: pass it through unchanged. */
601
+ export interface VoiceSession {
602
+ session_id: string;
603
+ url: string;
604
+ token: string;
605
+ expires_at: string;
606
+ max_seconds: number;
607
+ agent_name: string;
608
+ pages: Array<{
609
+ label: string;
610
+ path: string;
611
+ }>;
612
+ say_as: Record<string, string>;
613
+ }
614
+ /**
615
+ * The browser client for one of the account's apps, from createAppClient(): sign-in is under
616
+ * `auth`, and every data call runs as the signed-in person, inside the account's access rules.
617
+ */
449
618
  export declare class AwesomateAppClient {
450
619
  private readonly opts;
451
620
  private readonly base;
@@ -487,6 +656,7 @@ export declare class AwesomateAppClient {
487
656
  onChange: (listener: (user: AppUser | null) => void) => (() => void);
488
657
  /** Called when a sign-in link could not be used (expired, already used, not this app's). */
489
658
  onSignInError: (listener: (err: AwesomateError) => void) => (() => void);
659
+ /** Sign out here and end the session at the hub. */
490
660
  signOut: () => Promise<void>;
491
661
  /** A current access token, for the app's own server to verify against the JWKS. */
492
662
  accessToken: () => Promise<string | null>;
@@ -503,7 +673,15 @@ export declare class AwesomateAppClient {
503
673
  private refresh;
504
674
  /** 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. */
505
675
  private data;
676
+ /**
677
+ * Rows of one kind that this person may read, a page at a time. The database applies the
678
+ * kind's read rule for their role: a customer gets their own jobs, staff whatever theirs allows.
679
+ *
680
+ * @example
681
+ * const { rows } = await app.query('job', { orderBy: ['created_at', 'desc'], limit: 20 });
682
+ */
506
683
  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'>>;
684
+ /** Every matching row this person may read, a page at a time. maxRows (default 10,000) guards against reading a whole list by accident. */
507
685
  queryAll<K extends KindName, S extends keyof RowOf<K> = keyof RowOf<K>>(kind: K, options?: Omit<QueryOptions<RowOf<K>, S>, 'after'> & {
508
686
  maxRows?: number;
509
687
  }): AsyncGenerator<Pick<RowOf<K>, S>>;
@@ -511,6 +689,7 @@ export declare class AwesomateAppClient {
511
689
  get<K extends KindName>(kind: K, id: string): Promise<RowOf<K> | null>;
512
690
  /** Create a record (returns its id) or change one with options.id, inside the kind's write rule for this user. */
513
691
  write<K extends KindName>(kind: K, data: Partial<RowOf<K>> | Record<string, unknown>, options?: WriteOptions): Promise<string>;
692
+ /** Archive a record, inside the kind's write rule for this person. */
514
693
  archive<K extends KindName>(kind: K, id: string): Promise<void>;
515
694
  /**
516
695
  * Run a recipe the account opened to this person's role: "accept this quote", something their
@@ -529,6 +708,19 @@ export declare class AwesomateAppClient {
529
708
  ids?: string[];
530
709
  limit?: number;
531
710
  }): Promise<Customer[]>;
711
+ /**
712
+ * The agent this app talks with by voice, or null when the account has not picked one. Pass it
713
+ * to AwesomateAgent.mount as agentId; the hub decides which agent answers, never the page.
714
+ */
715
+ voiceAgent(): Promise<string | null>;
716
+ /**
717
+ * A voice session with the app's agent, as this signed-in person:
718
+ * `AwesomateAgent.mount({ agentId, session: () => app.voiceSession() })`. The agent greets them
719
+ * 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:
720
+ * no_voice_agent (the app has none), plan_required, consent_required, person_daily, agent_daily;
721
+ * its personMessage is the sentence to show the person, which the AwesomateAgent widget shows.
722
+ */
723
+ voiceSession(): Promise<VoiceSession>;
532
724
  /**
533
725
  * A list kept current: onRows gets the whole list (up to 200 rows, the query's order) at first
534
726
  * and again whenever a row in it is added, changed, removed, or stops being one this person may
@@ -546,6 +738,23 @@ export declare class AwesomateAppClient {
546
738
  */
547
739
  here(recordId: string, onPeople: (people: HerePerson[]) => void, onError?: (err: AwesomateError) => void): HereHandle;
548
740
  }
741
+ /**
742
+ * The browser client for one of the account's apps (Pro and above). Give it the app's publishable
743
+ * key, which is not a secret; never the account's token.
744
+ *
745
+ * @example
746
+ * const app = createAppClient({ publishableKey: 'pk_...' });
747
+ * app.auth.onChange((user) => render(user));
748
+ * await app.auth.signInWithLink(email, { redirectTo: location.href });
749
+ */
549
750
  export declare function createAppClient(options: AppClientOptions): AwesomateAppClient;
751
+ /**
752
+ * The server client: the account's token (amt_pat_... with crm:read) or an app's server key
753
+ * (ak_...). Both are secrets: keep them on a server, never in a browser.
754
+ *
755
+ * @example
756
+ * const db = createClient({ token: process.env.AWESOMATE_TOKEN! });
757
+ * const { rows } = await db.query('contact', { limit: 10 });
758
+ */
550
759
  export declare function createClient(options: ClientOptions): AwesomateClient;
551
760
  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.13.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
  }
@@ -215,6 +245,19 @@ export class AwesomateClient {
215
245
  async call(recipe, args = {}) {
216
246
  return (await this.request('POST', '/api/my-crm/v1/call', { fn: recipe, args }, false)).result;
217
247
  }
248
+ /**
249
+ * The business itself: the details Awesomate keeps about it (name, what it does, voice, colours,
250
+ * how customers reach it), grouped, each with where it came from. `confirmed` details are the
251
+ * ones agents and automations use; `suggestion` ones wait for the owner. Needs the account's
252
+ * token (hosting:read, every plan), never an app key.
253
+ */
254
+ business() {
255
+ return this.request('GET', '/api/my-business/v1/identity?format=json');
256
+ }
257
+ /** Details waiting for the owner's yes on Your business: from the website, the account, a team member or a file. */
258
+ async businessSuggestions() {
259
+ return (await this.request('GET', '/api/my-business/v1/proposals')).proposals;
260
+ }
218
261
  /** Grouped numbers (counts, sums) in the Business Data API's shape. */
219
262
  aggregate(request) {
220
263
  return this.request('POST', '/api/my-crm/v1/data/query', request);
@@ -241,6 +284,10 @@ export function signInTokenFrom(url) {
241
284
  return v && v.length >= 20 ? v : null;
242
285
  }
243
286
  const REFRESH_EARLY_MS = 30_000;
287
+ /**
288
+ * The browser client for one of the account's apps, from createAppClient(): sign-in is under
289
+ * `auth`, and every data call runs as the signed-in person, inside the account's access rules.
290
+ */
244
291
  export class AwesomateAppClient {
245
292
  opts;
246
293
  base;
@@ -338,6 +385,7 @@ export class AwesomateAppClient {
338
385
  this.linkErrorListeners.add(listener);
339
386
  return () => { this.linkErrorListeners.delete(listener); };
340
387
  },
388
+ /** Sign out here and end the session at the hub. */
341
389
  signOut: async () => {
342
390
  const s = await this.load();
343
391
  await this.clear();
@@ -369,7 +417,7 @@ export class AwesomateAppClient {
369
417
  return json;
370
418
  const server = typeof json.code === 'string' ? json.code : undefined;
371
419
  const message = typeof json.error === 'string' ? json.error : `The hub answered ${res.status}.`;
372
- throw new AwesomateError(errorCode(res.status, server), message, res.status, typeof json.field === 'string' ? json.field : undefined, server);
420
+ 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);
373
421
  }
374
422
  async read() {
375
423
  try {
@@ -475,10 +523,18 @@ export class AwesomateAppClient {
475
523
  return this.request(method, path, body, s.access_token);
476
524
  }
477
525
  }
526
+ /**
527
+ * Rows of one kind that this person may read, a page at a time. The database applies the
528
+ * kind's read rule for their role: a customer gets their own jobs, staff whatever theirs allows.
529
+ *
530
+ * @example
531
+ * const { rows } = await app.query('job', { orderBy: ['created_at', 'desc'], limit: 20 });
532
+ */
478
533
  async query(kind, options = {}) {
479
534
  const r = await this.data('POST', '/rows/query', { kind, ...options });
480
535
  return { rows: r.rows, next: r.next, count: r.count, applied: r.applied, asAt: r.as_at };
481
536
  }
537
+ /** Every matching row this person may read, a page at a time. maxRows (default 10,000) guards against reading a whole list by accident. */
482
538
  async *queryAll(kind, options = {}) {
483
539
  const { maxRows = 10_000, ...rest } = options;
484
540
  let after;
@@ -511,6 +567,7 @@ export class AwesomateAppClient {
511
567
  });
512
568
  return r.result.id;
513
569
  }
570
+ /** Archive a record, inside the kind's write rule for this person. */
514
571
  async archive(kind, id) {
515
572
  await this.data('POST', '/call', { fn: 'archive_record', args: { kind, id } });
516
573
  }
@@ -531,6 +588,23 @@ export class AwesomateAppClient {
531
588
  async lookupCustomers(options) {
532
589
  return (await this.data('POST', '/customers/lookup', options)).customers;
533
590
  }
591
+ /**
592
+ * The agent this app talks with by voice, or null when the account has not picked one. Pass it
593
+ * to AwesomateAgent.mount as agentId; the hub decides which agent answers, never the page.
594
+ */
595
+ async voiceAgent() {
596
+ return (await this.data('GET', '/me')).app.voice_agent_id ?? null;
597
+ }
598
+ /**
599
+ * A voice session with the app's agent, as this signed-in person:
600
+ * `AwesomateAgent.mount({ agentId, session: () => app.voiceSession() })`. The agent greets them
601
+ * 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:
602
+ * no_voice_agent (the app has none), plan_required, consent_required, person_daily, agent_daily;
603
+ * its personMessage is the sentence to show the person, which the AwesomateAgent widget shows.
604
+ */
605
+ async voiceSession() {
606
+ return this.data('POST', '/agents/sessions');
607
+ }
534
608
  /**
535
609
  * A list kept current: onRows gets the whole list (up to 200 rows, the query's order) at first
536
610
  * and again whenever a row in it is added, changed, removed, or stops being one this person may
@@ -842,9 +916,26 @@ class LiveSocket {
842
916
  this.failAll(new AwesomateError('unauthenticated', 'Signed out.', 401));
843
917
  }
844
918
  }
919
+ /**
920
+ * The browser client for one of the account's apps (Pro and above). Give it the app's publishable
921
+ * key, which is not a secret; never the account's token.
922
+ *
923
+ * @example
924
+ * const app = createAppClient({ publishableKey: 'pk_...' });
925
+ * app.auth.onChange((user) => render(user));
926
+ * await app.auth.signInWithLink(email, { redirectTo: location.href });
927
+ */
845
928
  export function createAppClient(options) {
846
929
  return new AwesomateAppClient(options);
847
930
  }
931
+ /**
932
+ * The server client: the account's token (amt_pat_... with crm:read) or an app's server key
933
+ * (ak_...). Both are secrets: keep them on a server, never in a browser.
934
+ *
935
+ * @example
936
+ * const db = createClient({ token: process.env.AWESOMATE_TOKEN! });
937
+ * const { rows } = await db.query('contact', { limit: 10 });
938
+ */
848
939
  export function createClient(options) {
849
940
  return new AwesomateClient(options);
850
941
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/sdk",
3
- "version": "0.11.0",
3
+ "version": "0.13.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,16 @@
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
+ "marked": "^18.0.14",
39
+ "shiki": "^4.5.0",
40
+ "typedoc": "^0.28.20",
36
41
  "typescript": "^5.7.2"
37
42
  }
38
43
  }