@awesomate/sdk 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +124 -3
- package/dist/index.js +110 -19
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* Docs: https://hub.awesomate.ai/docs/sdk/
|
|
25
25
|
*/
|
|
26
26
|
/** This package's version, sent to the hub with every server call. */
|
|
27
|
-
export declare const VERSION = "0.
|
|
27
|
+
export declare const VERSION = "0.19.0";
|
|
28
28
|
/** Augmented by the generated awesomate.d.ts, so each kind's rows are typed. */
|
|
29
29
|
export interface Kinds {
|
|
30
30
|
}
|
|
@@ -157,6 +157,87 @@ export interface Page<R> {
|
|
|
157
157
|
/** When the hub read the rows. */
|
|
158
158
|
asAt: string;
|
|
159
159
|
}
|
|
160
|
+
/** Someone on the account, as Tasks names them. */
|
|
161
|
+
export interface TaskPerson {
|
|
162
|
+
email: string;
|
|
163
|
+
name: string | null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* One card on the account's Tasks board: something waiting on a person (a quote to accept, an
|
|
167
|
+
* email to approve) or a task someone gave. The card's main action happens on its own page
|
|
168
|
+
* (`openPath` in the hub); Tasks only moves the card (not now, pass on) and finishes given tasks.
|
|
169
|
+
*/
|
|
170
|
+
export interface TaskCard {
|
|
171
|
+
/** Stable id of the card; pass it to notNow() and passOn(). */
|
|
172
|
+
key: string;
|
|
173
|
+
kind: string;
|
|
174
|
+
/** Where it came from, in the owner's words ("Build requests", "Contacts"). */
|
|
175
|
+
from: string;
|
|
176
|
+
title: string;
|
|
177
|
+
/** What is being asked, as a question. */
|
|
178
|
+
question: string;
|
|
179
|
+
context: string | null;
|
|
180
|
+
/** 1 routine to 5 urgent. */
|
|
181
|
+
priority: number;
|
|
182
|
+
createdAt: string;
|
|
183
|
+
dueAt: string | null;
|
|
184
|
+
/** The hub page that shows it in full. */
|
|
185
|
+
openPath: string;
|
|
186
|
+
/** Who has it, when it was passed on; null means anyone on the account. */
|
|
187
|
+
holder: TaskPerson | null;
|
|
188
|
+
/** When it comes back, if put off with Not now. */
|
|
189
|
+
snoozedUntil: string | null;
|
|
190
|
+
canAct: boolean;
|
|
191
|
+
/** Who it can be passed to. */
|
|
192
|
+
passTo: TaskPerson[];
|
|
193
|
+
/** Set for a task someone gave (give()); null for everything else. */
|
|
194
|
+
task: {
|
|
195
|
+
/** Pass it to update(), done() and drop(). */
|
|
196
|
+
id: number;
|
|
197
|
+
detail: string | null;
|
|
198
|
+
linkPath: string | null;
|
|
199
|
+
author: TaskPerson;
|
|
200
|
+
canFinish: boolean;
|
|
201
|
+
canDrop: boolean;
|
|
202
|
+
canEdit: boolean;
|
|
203
|
+
} | null;
|
|
204
|
+
}
|
|
205
|
+
/** The Tasks board, as the account's token sees it (the owner, so `scope: 'all'` works). */
|
|
206
|
+
export interface TaskBoard {
|
|
207
|
+
scope: 'mine' | 'all';
|
|
208
|
+
me: TaskPerson & {
|
|
209
|
+
role: string;
|
|
210
|
+
};
|
|
211
|
+
/** Who a task can be given to. */
|
|
212
|
+
people: Array<TaskPerson & {
|
|
213
|
+
me: boolean;
|
|
214
|
+
}>;
|
|
215
|
+
/** Waiting for a decision now. */
|
|
216
|
+
toDecide: TaskCard[];
|
|
217
|
+
/** Put off with Not now; back when the time comes. */
|
|
218
|
+
later: TaskCard[];
|
|
219
|
+
/** With someone else. */
|
|
220
|
+
waiting: TaskCard[];
|
|
221
|
+
/** Recently decided, for the record. */
|
|
222
|
+
decided: Array<{
|
|
223
|
+
key: string;
|
|
224
|
+
kind: string;
|
|
225
|
+
from: string;
|
|
226
|
+
text: string;
|
|
227
|
+
by: TaskPerson | null;
|
|
228
|
+
at: string;
|
|
229
|
+
}>;
|
|
230
|
+
}
|
|
231
|
+
/** A task to give: to someone on the account by email, or to the owner when `forEmail` is left out. */
|
|
232
|
+
export interface GiveTask {
|
|
233
|
+
title: string;
|
|
234
|
+
detail?: string;
|
|
235
|
+
forEmail?: string;
|
|
236
|
+
/** ISO time it is due. */
|
|
237
|
+
dueAt?: string;
|
|
238
|
+
/** A hub path the task points at, e.g. /contacts/people/... */
|
|
239
|
+
linkPath?: string;
|
|
240
|
+
}
|
|
160
241
|
/** A file or folder in the account's Files (the folders on its automation account: public/, private/, temp/). */
|
|
161
242
|
export interface FileEntry {
|
|
162
243
|
/** Relative to the files folder, starting with its top folder: public/images/logo.png. */
|
|
@@ -297,6 +378,44 @@ export declare class AwesomateClient {
|
|
|
297
378
|
trashPath: string;
|
|
298
379
|
}>;
|
|
299
380
|
};
|
|
381
|
+
private tasksCall;
|
|
382
|
+
/**
|
|
383
|
+
* The account's Tasks: what is waiting on its people, and tasks they give each other. An app's
|
|
384
|
+
* server or an n8n workflow can give someone a task ("call this customer back") and finish it.
|
|
385
|
+
* Needs the account's token, and Tasks on the account (a feature_unavailable refusal says
|
|
386
|
+
* when it is not).
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* await db.tasks.give({ title: 'Call Sam back about the leak', forEmail: 'jo@brightwater.example', dueAt: '2026-10-06T09:00:00+11:00' });
|
|
390
|
+
*/
|
|
391
|
+
readonly tasks: {
|
|
392
|
+
/** The board: waiting now, put off, with someone else, recently decided. `all` shows everyone's, not only yours. */
|
|
393
|
+
board: (options?: {
|
|
394
|
+
scope?: "mine" | "all";
|
|
395
|
+
}) => Promise<TaskBoard>;
|
|
396
|
+
/** Give a task. The person gets it on their board, and an email when they are not the one giving it. */
|
|
397
|
+
give: (task: GiveTask) => Promise<{
|
|
398
|
+
key: string;
|
|
399
|
+
id: number;
|
|
400
|
+
holder: TaskPerson;
|
|
401
|
+
emailed: boolean;
|
|
402
|
+
}>;
|
|
403
|
+
/** Change a given task: whoever wrote it, or the owner. */
|
|
404
|
+
update: (id: number | string, patch: Partial<Omit<GiveTask, "forEmail">>) => Promise<void>;
|
|
405
|
+
/** Mark a given task done: the person who has it, whoever wrote it, or the owner. */
|
|
406
|
+
done: (id: number | string) => Promise<void>;
|
|
407
|
+
/** Take a given task off the board without doing it: whoever wrote it, or the owner. */
|
|
408
|
+
drop: (id: number | string) => Promise<void>;
|
|
409
|
+
/** Put a card off until a time (at most 90 days), for this person only. */
|
|
410
|
+
notNow: (key: string, until: Date | string) => Promise<{
|
|
411
|
+
until: string;
|
|
412
|
+
}>;
|
|
413
|
+
/** Hand a card to someone on the account who can act on it. */
|
|
414
|
+
passOn: (key: string, toEmail: string, reason?: string) => Promise<{
|
|
415
|
+
holder: TaskPerson;
|
|
416
|
+
emailed: boolean;
|
|
417
|
+
}>;
|
|
418
|
+
};
|
|
300
419
|
/** The kinds this token can read, their columns, operators and examples. */
|
|
301
420
|
schema(): Promise<{
|
|
302
421
|
kinds: Array<{
|
|
@@ -830,7 +949,7 @@ export interface HereHandle {
|
|
|
830
949
|
* stopped (on send or blur). Without another call, typing ends on its own after a few seconds.
|
|
831
950
|
*/
|
|
832
951
|
typing(on: boolean): void;
|
|
833
|
-
/** Close the record: the others stop seeing this person on it. */
|
|
952
|
+
/** Close the record: the others stop seeing this person on it once every part of the page that opened it has left. */
|
|
834
953
|
leave(): void;
|
|
835
954
|
}
|
|
836
955
|
/** What live() calls. */
|
|
@@ -1007,7 +1126,9 @@ export declare class AwesomateAppClient {
|
|
|
1007
1126
|
* Open a record (a job's conversation) for who-is-here: onPeople gets everyone else who has it
|
|
1008
1127
|
* open with their tab in front, and who is typing, at first and on every change; the app's
|
|
1009
1128
|
* assistant appears while it writes a reply. The hub checks this person may read the record, and
|
|
1010
|
-
* keeps checking; onError hears a refusal. Shares the one live connection.
|
|
1129
|
+
* keeps checking; onError hears a refusal. Shares the one live connection. Two parts of a page (a
|
|
1130
|
+
* thread and a sidebar) can open the same record: each hears onPeople, and it stays open until both
|
|
1131
|
+
* have left. Returns typing() and leave().
|
|
1011
1132
|
*/
|
|
1012
1133
|
here(recordId: string, onPeople: (people: HerePerson[]) => void, onError?: (err: AwesomateError) => void): HereHandle;
|
|
1013
1134
|
}
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* Docs: https://hub.awesomate.ai/docs/sdk/
|
|
25
25
|
*/
|
|
26
26
|
/** This package's version, sent to the hub with every server call. */
|
|
27
|
-
export const VERSION = '0.
|
|
27
|
+
export const VERSION = '0.19.0';
|
|
28
28
|
const DEFAULT_BASE = 'https://hub.awesomate.ai';
|
|
29
29
|
const ERROR_CODES = ['unauthenticated', 'forbidden', 'not_found', 'validation', 'consent_blocked', 'rate_limited', 'conflict', 'unavailable'];
|
|
30
30
|
/**
|
|
@@ -230,6 +230,56 @@ export class AwesomateClient {
|
|
|
230
230
|
/** Move to the trash. Not erased, and no longer counted toward the Files space. */
|
|
231
231
|
remove: async (path) => this.filesCall('DELETE', `/api/files?path=${encodeURIComponent(path)}`),
|
|
232
232
|
};
|
|
233
|
+
tasksCall(method, path, body) {
|
|
234
|
+
if (this.appKey) {
|
|
235
|
+
return Promise.reject(new AwesomateError('forbidden', 'Tasks needs the account\'s token (amt_pat_...), not an app key.', 0));
|
|
236
|
+
}
|
|
237
|
+
return this.request(method, path, body, false);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The account's Tasks: what is waiting on its people, and tasks they give each other. An app's
|
|
241
|
+
* server or an n8n workflow can give someone a task ("call this customer back") and finish it.
|
|
242
|
+
* Needs the account's token, and Tasks on the account (a feature_unavailable refusal says
|
|
243
|
+
* when it is not).
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* await db.tasks.give({ title: 'Call Sam back about the leak', forEmail: 'jo@brightwater.example', dueAt: '2026-10-06T09:00:00+11:00' });
|
|
247
|
+
*/
|
|
248
|
+
tasks = {
|
|
249
|
+
/** The board: waiting now, put off, with someone else, recently decided. `all` shows everyone's, not only yours. */
|
|
250
|
+
board: (options = {}) => this.tasksCall('GET', `/api/my-tasks${options.scope === 'all' ? '?scope=all' : ''}`),
|
|
251
|
+
/** Give a task. The person gets it on their board, and an email when they are not the one giving it. */
|
|
252
|
+
give: async (task) => {
|
|
253
|
+
const r = await this.tasksCall('POST', '/api/my-tasks/tasks', task);
|
|
254
|
+
return { key: r.key, id: r.id, holder: r.holder, emailed: r.emailed };
|
|
255
|
+
},
|
|
256
|
+
/** Change a given task: whoever wrote it, or the owner. */
|
|
257
|
+
update: async (id, patch) => {
|
|
258
|
+
await this.tasksCall('PATCH', `/api/my-tasks/tasks/${encodeURIComponent(String(id))}`, patch);
|
|
259
|
+
},
|
|
260
|
+
/** Mark a given task done: the person who has it, whoever wrote it, or the owner. */
|
|
261
|
+
done: async (id) => {
|
|
262
|
+
await this.tasksCall('POST', `/api/my-tasks/tasks/${encodeURIComponent(String(id))}/done`);
|
|
263
|
+
},
|
|
264
|
+
/** Take a given task off the board without doing it: whoever wrote it, or the owner. */
|
|
265
|
+
drop: async (id) => {
|
|
266
|
+
await this.tasksCall('POST', `/api/my-tasks/tasks/${encodeURIComponent(String(id))}/drop`);
|
|
267
|
+
},
|
|
268
|
+
/** Put a card off until a time (at most 90 days), for this person only. */
|
|
269
|
+
notNow: async (key, until) => {
|
|
270
|
+
const r = await this.tasksCall('POST', `/api/my-tasks/items/${encodeURIComponent(key)}/not-now`, {
|
|
271
|
+
until: until instanceof Date ? until.toISOString() : until,
|
|
272
|
+
});
|
|
273
|
+
return { until: r.until };
|
|
274
|
+
},
|
|
275
|
+
/** Hand a card to someone on the account who can act on it. */
|
|
276
|
+
passOn: async (key, toEmail, reason) => {
|
|
277
|
+
const r = await this.tasksCall('POST', `/api/my-tasks/items/${encodeURIComponent(key)}/pass-on`, {
|
|
278
|
+
toEmail, ...(reason ? { reason } : {}),
|
|
279
|
+
});
|
|
280
|
+
return { holder: r.holder, emailed: r.emailed };
|
|
281
|
+
},
|
|
282
|
+
};
|
|
233
283
|
/** The kinds this token can read, their columns, operators and examples. */
|
|
234
284
|
schema() {
|
|
235
285
|
return this.request('GET', '/api/my-crm/v1/rows/schema');
|
|
@@ -271,7 +321,7 @@ export class AwesomateClient {
|
|
|
271
321
|
async get(kind, id, options = {}) {
|
|
272
322
|
try {
|
|
273
323
|
const qs = options.tz ? `?tz=${encodeURIComponent(options.tz)}` : '';
|
|
274
|
-
const r = await this.request('GET', `/api/my-crm/v1/rows/${encodeURIComponent(kind)}/${encodeURIComponent(id)}${qs}`);
|
|
324
|
+
const r = await this.request('GET', `/api/my-crm/v1/rows/${encodeURIComponent(kind)}/${encodeURIComponent(String(id))}${qs}`);
|
|
275
325
|
return r.row;
|
|
276
326
|
}
|
|
277
327
|
catch (err) {
|
|
@@ -658,7 +708,7 @@ export class AwesomateAppClient {
|
|
|
658
708
|
/** One record by id, or null when there is none this user may read. */
|
|
659
709
|
async get(kind, id) {
|
|
660
710
|
try {
|
|
661
|
-
return (await this.data('GET', `/rows/${encodeURIComponent(kind)}/${encodeURIComponent(id)}`)).row;
|
|
711
|
+
return (await this.data('GET', `/rows/${encodeURIComponent(kind)}/${encodeURIComponent(String(id))}`)).row;
|
|
662
712
|
}
|
|
663
713
|
catch (err) {
|
|
664
714
|
if (err instanceof AwesomateError && err.code === 'not_found')
|
|
@@ -799,7 +849,9 @@ export class AwesomateAppClient {
|
|
|
799
849
|
* Open a record (a job's conversation) for who-is-here: onPeople gets everyone else who has it
|
|
800
850
|
* open with their tab in front, and who is typing, at first and on every change; the app's
|
|
801
851
|
* assistant appears while it writes a reply. The hub checks this person may read the record, and
|
|
802
|
-
* keeps checking; onError hears a refusal. Shares the one live connection.
|
|
852
|
+
* keeps checking; onError hears a refusal. Shares the one live connection. Two parts of a page (a
|
|
853
|
+
* thread and a sidebar) can open the same record: each hears onPeople, and it stays open until both
|
|
854
|
+
* have left. Returns typing() and leave().
|
|
803
855
|
*/
|
|
804
856
|
here(recordId, onPeople, onError) {
|
|
805
857
|
return this.liveSocket().join(recordId, onPeople, onError);
|
|
@@ -842,32 +894,57 @@ class LiveSocket {
|
|
|
842
894
|
if (this.ready && typeof d?.visibilityState === 'string')
|
|
843
895
|
this.send({ type: 'presence', visible: d.visibilityState === 'visible' });
|
|
844
896
|
}
|
|
845
|
-
/**
|
|
897
|
+
/**
|
|
898
|
+
* Records open with here(), one per record id however many parts of the page opened it: the hub
|
|
899
|
+
* hears `here on` once and `here off` when the last of them leaves, and every one of them gets
|
|
900
|
+
* onPeople. Typing is the person's, so one resend clock per record, shared by them all.
|
|
901
|
+
*/
|
|
846
902
|
rooms = new Map();
|
|
847
903
|
get wanted() {
|
|
848
904
|
return this.entries.size > 0 || this.rooms.size > 0;
|
|
849
905
|
}
|
|
850
906
|
join(id, onPeople, onError) {
|
|
851
|
-
const
|
|
852
|
-
this.rooms.
|
|
853
|
-
if (
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
907
|
+
const listener = { onPeople, onError };
|
|
908
|
+
let room = this.rooms.get(id);
|
|
909
|
+
if (room) {
|
|
910
|
+
room.listeners.add(listener);
|
|
911
|
+
// Already open: the hub sends nothing new for a second join, so hand over who it last said was there.
|
|
912
|
+
const people = room.people;
|
|
913
|
+
if (people)
|
|
914
|
+
queueMicrotask(() => { if (this.rooms.get(id)?.listeners.has(listener))
|
|
915
|
+
onPeople(people); });
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
room = { listeners: new Set([listener]), typingSentAt: 0, people: null };
|
|
919
|
+
this.rooms.set(id, room);
|
|
920
|
+
if (this.ready)
|
|
921
|
+
this.send({ type: 'here', id, on: true });
|
|
922
|
+
else if (!this.ws && !this.connecting)
|
|
923
|
+
void this.connect();
|
|
924
|
+
}
|
|
925
|
+
const open = () => {
|
|
926
|
+
const r = this.rooms.get(id);
|
|
927
|
+
return r?.listeners.has(listener) ? r : undefined;
|
|
928
|
+
};
|
|
857
929
|
return {
|
|
858
930
|
typing: (on) => {
|
|
859
|
-
|
|
931
|
+
const r = open();
|
|
932
|
+
if (!r || !this.ready)
|
|
860
933
|
return;
|
|
861
934
|
const now = Date.now();
|
|
862
|
-
if (on && now -
|
|
935
|
+
if (on && now - r.typingSentAt < TYPING_RESEND_MS)
|
|
863
936
|
return;
|
|
864
|
-
if (!on && !
|
|
937
|
+
if (!on && !r.typingSentAt)
|
|
865
938
|
return;
|
|
866
|
-
|
|
939
|
+
r.typingSentAt = on ? now : 0;
|
|
867
940
|
this.send({ type: 'typing', id, on });
|
|
868
941
|
},
|
|
869
942
|
leave: () => {
|
|
870
|
-
|
|
943
|
+
const r = open();
|
|
944
|
+
if (!r)
|
|
945
|
+
return;
|
|
946
|
+
r.listeners.delete(listener);
|
|
947
|
+
if (r.listeners.size)
|
|
871
948
|
return;
|
|
872
949
|
this.rooms.delete(id);
|
|
873
950
|
if (this.ready)
|
|
@@ -948,7 +1025,13 @@ class LiveSocket {
|
|
|
948
1025
|
return;
|
|
949
1026
|
}
|
|
950
1027
|
if (f.type === 'people' && typeof f.id === 'string') {
|
|
951
|
-
this.rooms.get(f.id)
|
|
1028
|
+
const room = this.rooms.get(f.id);
|
|
1029
|
+
if (!room)
|
|
1030
|
+
return;
|
|
1031
|
+
const people = Array.isArray(f.people) ? f.people : [];
|
|
1032
|
+
room.people = people;
|
|
1033
|
+
for (const l of [...room.listeners])
|
|
1034
|
+
l.onPeople(people);
|
|
952
1035
|
return;
|
|
953
1036
|
}
|
|
954
1037
|
if (f.type === 'error' && typeof f.id === 'string' && f.id.startsWith('here:')) {
|
|
@@ -958,7 +1041,9 @@ class LiveSocket {
|
|
|
958
1041
|
return;
|
|
959
1042
|
this.rooms.delete(id);
|
|
960
1043
|
const code = ERROR_CODES.includes(String(f.code)) ? f.code : 'unavailable';
|
|
961
|
-
|
|
1044
|
+
const err = new AwesomateError(code, String(f.message ?? 'Refused.'), 0, undefined, String(f.code));
|
|
1045
|
+
for (const l of room.listeners)
|
|
1046
|
+
l.onError?.(err);
|
|
962
1047
|
if (!this.wanted)
|
|
963
1048
|
this.shut();
|
|
964
1049
|
return;
|
|
@@ -1013,6 +1098,8 @@ class LiveSocket {
|
|
|
1013
1098
|
if (this.retry)
|
|
1014
1099
|
clearTimeout(this.retry);
|
|
1015
1100
|
this.pinger = this.retry = null;
|
|
1101
|
+
for (const room of this.rooms.values())
|
|
1102
|
+
room.people = null;
|
|
1016
1103
|
if (ws && ws.readyState <= 1) {
|
|
1017
1104
|
try {
|
|
1018
1105
|
ws.send(JSON.stringify({ type: 'bye' }));
|
|
@@ -1039,6 +1126,9 @@ class LiveSocket {
|
|
|
1039
1126
|
if (this.pinger)
|
|
1040
1127
|
clearInterval(this.pinger);
|
|
1041
1128
|
this.pinger = null;
|
|
1129
|
+
// Who was there is the old connection's word: a part opening a record now waits for the new one.
|
|
1130
|
+
for (const room of this.rooms.values())
|
|
1131
|
+
room.people = null;
|
|
1042
1132
|
if (this.stopped || !this.wanted)
|
|
1043
1133
|
return;
|
|
1044
1134
|
if (code === 4401) {
|
|
@@ -1060,7 +1150,8 @@ class LiveSocket {
|
|
|
1060
1150
|
for (const e of all)
|
|
1061
1151
|
e.handlers.onError?.(err);
|
|
1062
1152
|
for (const r of rooms)
|
|
1063
|
-
r.
|
|
1153
|
+
for (const l of r.listeners)
|
|
1154
|
+
l.onError?.(err);
|
|
1064
1155
|
this.shut();
|
|
1065
1156
|
}
|
|
1066
1157
|
shut() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awesomate/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|