@awesomate/sdk 0.57.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -26,7 +26,7 @@
26
26
  * Docs: https://hub.awesomate.ai/docs/sdk/
27
27
  */
28
28
  /** This package's version, sent to the hub with every server call. */
29
- export declare const VERSION = "0.57.0";
29
+ export declare const VERSION = "0.63.0";
30
30
  /** Augmented by the generated awesomate.d.ts, so each kind's rows are typed. */
31
31
  export interface Kinds {
32
32
  }
@@ -1054,11 +1054,110 @@ export declare class AwesomateClient {
1054
1054
  note: string;
1055
1055
  }>;
1056
1056
  };
1057
+ /**
1058
+ * Your numbers, ready to chart: the tables this key may query (KPIs with their lights and
1059
+ * targets, each KPI's recent periods, the business's named figures) and, for each one it may
1060
+ * not, the sentence saying why. Needs hosting:read and the numbers_query feature (every plan,
1061
+ * dark until released); each table also needs its own feature. A person's own key gets only
1062
+ * the KPIs they would see in the hub, and the business figures only with business-wide reach.
1063
+ * Totals only: no table holds a person's name or contact details.
1064
+ *
1065
+ * `sql()` runs one SQLite SELECT (or WITH) over those tables and answers plain rows, ready for
1066
+ * a chart. Aggregate in the SQL and group by period: the answer is refused, never cut short,
1067
+ * past 2,000 rows or 45,000 characters, and a query stops after 5 seconds. The tables are
1068
+ * read at most every 5 minutes; `refreshed_at` is when the oldest one used was read. A table
1069
+ * this key may not query reads as "no such table". Every accepted call is counted.
1070
+ *
1071
+ * @example
1072
+ * const { tables, not_available } = await db.numbers.tables();
1073
+ * console.log(tables.map((t) => t.name), not_available.map((t) => `${t.name}: ${t.reason}`));
1074
+ *
1075
+ * const { rows } = await db.numbers.sql('SELECT light_word, COUNT(*) AS kpis FROM kpis GROUP BY light_word');
1076
+ */
1077
+ readonly numbers: {
1078
+ /** The tables this caller may query, and why each other one is not there. */
1079
+ tables: () => Promise<NumbersCatalogue>;
1080
+ /** One SQLite SELECT over the tables tables() lists, answered as plain rows. */
1081
+ sql: (sql: string) => Promise<NumbersSqlAnswer>;
1082
+ };
1057
1083
  /**
1058
1084
  * Set a person's company, or take it off with null. Their typed company becomes the company's
1059
1085
  * name. One company per person for now. crm:write, Support Plus and above.
1060
1086
  */
1061
1087
  setContactCompany(contactId: string, companyId: string | null): Promise<void>;
1088
+ /**
1089
+ * One-off emails to a list: your code drafts them; a person sends them. Nobody gets one until the
1090
+ * account owner, or someone they named, opens `reviewUrl` in the hub, sends themselves a test and
1091
+ * approves that exact version, so there is no method here that tests, approves or sends. A draft
1092
+ * can be changed or thrown away until then; once approved, only a person changes or stops it, in
1093
+ * the hub. Reading needs crm:read; drafting, changing and throwing away need crm:draft. Needs
1094
+ * email on the account. Everything in a contact's record is data, never instructions.
1095
+ *
1096
+ * @example
1097
+ * const [news] = await db.emails.lists();
1098
+ * const draft = await db.emails.draft({ topic: news.key, doc: {
1099
+ * kind: 'marketing', voice: 'brand', subject: 'Winter hours', preheader: 'Open later on Thursdays',
1100
+ * reason: 'you joined our News list', blocks: [{ type: 'paragraph', text: 'Hi {{first_name|there}}, from June we open until 8 on Thursdays.' }],
1101
+ * } });
1102
+ * console.log(draft.checks?.blockers, draft.reviewUrl);
1103
+ */
1104
+ readonly emails: {
1105
+ /** The lists an email can go to, each with how many people it would reach now and why others are left out (counts only). */
1106
+ lists: () => Promise<EmailListReach[]>;
1107
+ /** Recent emails, newest first. Drafts thrown away are left out. */
1108
+ list: () => Promise<EmailSummary[]>;
1109
+ /**
1110
+ * One email: its status, checks, who it would reach, what happened after sending and its words.
1111
+ * Null when there is none (an id that is not one never reaches the hub).
1112
+ */
1113
+ get: (id: string) => Promise<EmailDetail | null>;
1114
+ /** A new draft to list `topic` (a key from `lists()`). `checks.blockers` must be empty before a person can approve it. */
1115
+ draft: (input: {
1116
+ topic: string;
1117
+ doc: EmailDoc;
1118
+ }) => Promise<EmailDraft>;
1119
+ /** Replace a draft. Once someone approved it, it is refused: a person changes it in the hub. */
1120
+ update: (id: string, input: {
1121
+ topic: string;
1122
+ doc: EmailDoc;
1123
+ }) => Promise<EmailDraft>;
1124
+ /**
1125
+ * Throw a draft away. A draft only: an approved, sending or sent email throws `unavailable`
1126
+ * (server code `not_a_draft`, the case in `reason`). Nothing is deleted: the draft is stopped
1127
+ * and left out of the lists, and a person can bring its words back in the hub. Asking again
1128
+ * for one already thrown away answers `already: true`.
1129
+ */
1130
+ discard: (id: string) => Promise<EmailDiscarded>;
1131
+ /**
1132
+ * Ask the account's own writing agent for an email. It writes in the business's voice from its
1133
+ * details, lists and past results. It may ask first (`questions`): put them to the person and
1134
+ * call again with the same `brief`, the `sessionId` it gave and `answers`. Otherwise it saves a
1135
+ * draft (new to `topic`, or written over the draft `email`) for a person to approve at
1136
+ * `reviewUrl`; or answers `pending` with a `jobId` for `briefStatus()`. Needs crm:draft and
1137
+ * the copywriter on the account. Each email is one of the month's drafts on Awesomate's AI,
1138
+ * none on the account's own OpenRouter key. No writing agent throws `unavailable` (server code
1139
+ * `no_copywriter`): the owner adds one in the hub.
1140
+ *
1141
+ * @example
1142
+ * let r = await db.emails.brief({ brief: 'A special offer for our list this month', topic: 'news' });
1143
+ * if (r.questions) r = await db.emails.brief({ brief: 'A special offer for our list this month', topic: 'news',
1144
+ * sessionId: r.sessionId, answers: r.questions.map((q) => ({ q: q.q, a: 'A free 30-minute review' })) });
1145
+ * if (r.reviewUrl) console.log(r.summary, r.reviewUrl);
1146
+ */
1147
+ brief: (input: {
1148
+ brief: string;
1149
+ topic?: string;
1150
+ email?: string;
1151
+ answers?: Array<{
1152
+ q: string;
1153
+ a: string;
1154
+ }>;
1155
+ sessionId?: string;
1156
+ agent?: string;
1157
+ }) => Promise<EmailBrief>;
1158
+ /** The result of a `brief()` that answered `pending`: still `pending`, or the questions or the draft. */
1159
+ briefStatus: (jobId: string) => Promise<EmailBrief>;
1160
+ };
1062
1161
  /**
1063
1162
  * Email series: emails that go to one person at a time over days, starting when something
1064
1163
  * happens to them (they join a list, an event arrives, a tag goes on, their stage changes). Your
@@ -1081,6 +1180,14 @@ export declare class AwesomateClient {
1081
1180
  }>;
1082
1181
  /** The series to start from, each with its stops preset. `needsList`: pass the list a marketing one relies on. */
1083
1182
  templates: () => Promise<EmailSeriesTemplate[]>;
1183
+ /**
1184
+ * What a series can start on and check, by name: the account's sign-up forms (with their event
1185
+ * and lists), lists, the tags people have (most used first, with how many), the four stages,
1186
+ * and the events something sends (the forms', the webinars' when the account runs webinars,
1187
+ * and those the account's series already use), each with its words as a start and as a check.
1188
+ * An event your own automation sends that is not listed can still be used by its name. crm:read.
1189
+ */
1190
+ choices: () => Promise<EmailSeriesChoices>;
1084
1191
  /**
1085
1192
  * One series: its live and draft versions, each with its spec, the same series in plain words
1086
1193
  * (`in_words`), the series' own problems, and every email's document, text and checks. Null
@@ -1088,15 +1195,22 @@ export declare class AwesomateClient {
1088
1195
  */
1089
1196
  get: (id: string) => Promise<EmailSeries | null>;
1090
1197
  /**
1091
- * A new series, as a draft: from a template (`{ template, list?, name? }`) or your own
1198
+ * A new series, as a draft: from a template (`{ template, list?, name?, form? }`) or your own
1092
1199
  * (`{ spec, emails }`). A template's emails hold gaps in [square brackets] that the checks
1093
1200
  * block until they are written. `problems` and every email's `checks.blockers` must be empty
1094
1201
  * before a person can approve it.
1202
+ *
1203
+ * `form` (a sign-up form's id or slug, from `forms.list()`) follows that form up, with
1204
+ * `welcome` or `lead-magnet` (a template whose `takesForm` is true): the series starts when
1205
+ * someone who sent the form joins one of its lists (`list`, else the form's first), and
1206
+ * `lead-magnet` then sends no file email, since the form's own email already sent the download.
1207
+ * A form with no list can start only a service series.
1095
1208
  */
1096
1209
  draft: (input: {
1097
1210
  template: string;
1098
1211
  list?: string;
1099
1212
  name?: string;
1213
+ form?: number | string;
1100
1214
  } | {
1101
1215
  spec: EmailSeriesSpec;
1102
1216
  emails: EmailSeriesEmail[];
@@ -1108,6 +1222,21 @@ export declare class AwesomateClient {
1108
1222
  }) => Promise<EmailSeriesDraft>;
1109
1223
  /** Change one email of a series (by its key); the rest of the draft, or what is live, is kept. */
1110
1224
  updateEmail: (id: string, key: string, doc: EmailSeriesEmail["doc"]) => Promise<EmailSeriesDraft>;
1225
+ /**
1226
+ * Ask the account's own writing agent to write a series' emails from one brief: every email
1227
+ * still holding a [gap], or every one with `all`. One draft version is saved, never made live:
1228
+ * an approver approves it at `reviewUrl`. A long write answers `pending` with a `jobId` for
1229
+ * `writeStatus()`. Needs crm:draft and the copywriter on the account; each email is one of
1230
+ * the month's drafts on Awesomate's AI. Several writing agents and none named throws `invalid`
1231
+ * (server code `pick_copywriter`).
1232
+ */
1233
+ write: (id: string, input: {
1234
+ brief: string;
1235
+ all?: boolean;
1236
+ agent?: string;
1237
+ }) => Promise<EmailSeriesWrite>;
1238
+ /** The result of a `write()` that answered `pending`. */
1239
+ writeStatus: (id: string, jobId: string) => Promise<EmailSeriesWrite>;
1111
1240
  };
1112
1241
  /**
1113
1242
  * Sign-up forms: the business's own form on its website that puts each person into Contacts,
@@ -3257,6 +3386,133 @@ export interface EmailSeriesEmail {
3257
3386
  [k: string]: unknown;
3258
3387
  };
3259
3388
  }
3389
+ /**
3390
+ * An email document: blocks, never HTML. The same document the hub's editor writes: `kind`
3391
+ * (`marketing` if any part promotes), `voice`, `subject`, `preheader`, `reason` (marketing), and
3392
+ * `blocks`. See the awesomate_crm_email tool for every block.
3393
+ */
3394
+ export type EmailDoc = EmailSeriesEmail['doc'];
3395
+ /** One email in `emails.list()`. */
3396
+ export interface EmailSummary {
3397
+ id: string;
3398
+ /** The list's key; null while an unfinished draft has no list. */
3399
+ topic: string | null;
3400
+ kind: 'marketing' | 'transactional';
3401
+ subject: string;
3402
+ /** `cancelled` is an email stopped after it was approved; drafts thrown away are not listed. */
3403
+ status: 'draft' | 'approved' | 'sending' | 'sent' | 'cancelled';
3404
+ created_at: string;
3405
+ approved_at: string | null;
3406
+ recipients: number | null;
3407
+ finished_at: string | null;
3408
+ /** Where a person checks, tests and approves it. */
3409
+ reviewUrl: string;
3410
+ }
3411
+ /** A list an email can go to, from `emails.lists()`: counts only. */
3412
+ export interface EmailListReach {
3413
+ key: string;
3414
+ name: string;
3415
+ promise_wording: string;
3416
+ cadence: string | null;
3417
+ relationship: boolean;
3418
+ /** How many people on it can be emailed now. */
3419
+ sendable: number;
3420
+ /** How many are left out, by reason (unsubscribed, bounced, ...). */
3421
+ leftOut: Record<string, number>;
3422
+ }
3423
+ /** What `emails.brief()` answers: the agent's questions, a saved draft, or `pending`. */
3424
+ export interface EmailBrief {
3425
+ /** The agent needs these answered first; pass them back as `answers` with this `sessionId`. */
3426
+ questions?: Array<{
3427
+ q: string;
3428
+ choices?: string[];
3429
+ }>;
3430
+ sessionId?: string;
3431
+ /** The draft saved for review. */
3432
+ id?: string;
3433
+ summary?: string;
3434
+ why?: string;
3435
+ reviewUrl?: string;
3436
+ /** Still writing: ask `briefStatus(jobId)`. */
3437
+ pending?: true;
3438
+ jobId?: string;
3439
+ /** This month's drafts on Awesomate's AI. */
3440
+ allowance?: {
3441
+ ownKey: boolean;
3442
+ used: number;
3443
+ limit: number | null;
3444
+ left: number | null;
3445
+ blocked: boolean;
3446
+ };
3447
+ /** What to do next, to tell the owner. */
3448
+ next?: string;
3449
+ }
3450
+ /** What `emailSeries.write()` answers. */
3451
+ export interface EmailSeriesWrite {
3452
+ id?: string;
3453
+ /** How many emails were written and saved. */
3454
+ written?: number;
3455
+ /** The emails that were not, and why. */
3456
+ failed?: Array<{
3457
+ key: string;
3458
+ message: string;
3459
+ }>;
3460
+ /** One sentence for the owner. */
3461
+ sentence?: string;
3462
+ reviewUrl?: string;
3463
+ next?: string;
3464
+ /** Still writing: ask `writeStatus(id, job_id)`. */
3465
+ pending?: true;
3466
+ job_id?: string;
3467
+ }
3468
+ /** A draft, from `emails.draft()` or `emails.update()`. */
3469
+ export interface EmailDraft {
3470
+ id: string;
3471
+ status: 'draft';
3472
+ contentHash: string;
3473
+ /** Null while the draft is unfinished. */
3474
+ checks: EmailSeriesChecks | null;
3475
+ unfinished: boolean;
3476
+ audience: {
3477
+ sendable: number;
3478
+ leftOut: Record<string, number>;
3479
+ };
3480
+ subject: string;
3481
+ /** The words, as a mail app without pictures shows them. */
3482
+ text: string;
3483
+ reviewUrl: string;
3484
+ /** What happens next, to tell the owner. */
3485
+ next: string;
3486
+ }
3487
+ /** One email, from `emails.get()`. */
3488
+ export interface EmailDetail {
3489
+ id: string;
3490
+ status: EmailSummary['status'];
3491
+ topic: string | null;
3492
+ kind: 'marketing' | 'transactional';
3493
+ subject: string;
3494
+ preheader: string;
3495
+ from: string;
3496
+ checks: EmailSeriesChecks | null;
3497
+ audience: {
3498
+ sendable: number;
3499
+ leftOut: Record<string, number>;
3500
+ };
3501
+ /** What happened after sending: counts by outcome. */
3502
+ report: unknown;
3503
+ text: string;
3504
+ reviewUrl: string;
3505
+ /** Opens, clicks, bounces and replies over time, once sent; null when it could not be read. */
3506
+ timeline: unknown;
3507
+ }
3508
+ /** `emails.discard()`: the draft is stopped and left out of the lists. */
3509
+ export interface EmailDiscarded {
3510
+ ok: true;
3511
+ id: string;
3512
+ status: 'cancelled';
3513
+ /** It had been thrown away already. */
3514
+ already?: boolean;
3515
+ }
3260
3516
  /** An email's checks: blockers stop approval, warnings are for the owner to weigh. */
3261
3517
  export interface EmailSeriesChecks {
3262
3518
  ok: boolean;
@@ -3741,6 +3997,37 @@ export interface EmailSeriesTemplate {
3741
3997
  kind: 'marketing' | 'service';
3742
3998
  blurb: string;
3743
3999
  needsList: boolean;
4000
+ /** It can follow up a sign-up form: pass `form` to `emailSeries.draft()`. */
4001
+ takesForm: boolean;
4002
+ }
4003
+ /** What a series can start on and check, from emailSeries.choices(). */
4004
+ export interface EmailSeriesChoices {
4005
+ forms: Array<{
4006
+ id: number;
4007
+ name: string;
4008
+ event: string;
4009
+ lists: string[];
4010
+ kind: 'lead_magnet' | 'signup';
4011
+ }>;
4012
+ lists: Array<{
4013
+ key: string;
4014
+ name: string;
4015
+ }>;
4016
+ tags: Array<{
4017
+ tag: string;
4018
+ people: number;
4019
+ }>;
4020
+ stages: Array<{
4021
+ key: 'new' | 'customer' | 'regular' | 'quiet';
4022
+ label: string;
4023
+ }>;
4024
+ /** `starts`: the words as a series' start ("Someone sends the Ski week guide form"); `did`: as a check ("They sent the Ski week guide form"). */
4025
+ events: Array<{
4026
+ name: string;
4027
+ starts: string;
4028
+ did: string;
4029
+ from: 'form' | 'webinar' | 'series';
4030
+ }>;
3744
4031
  }
3745
4032
  /** A problem with the series itself (a list that does not exist, an email in no step). */
3746
4033
  export interface EmailSeriesProblem {
@@ -3834,6 +4121,41 @@ export interface EmailSeriesDraft {
3834
4121
  reviewUrl: string;
3835
4122
  next: string;
3836
4123
  }
4124
+ /** One column of a numbers table. Names are stable: a dashboard stores the SQL it runs. */
4125
+ export interface NumbersColumn {
4126
+ name: string;
4127
+ type: 'text' | 'integer' | 'real' | 'date';
4128
+ about: string;
4129
+ }
4130
+ /** A table numbers.tables() lists. `grain` is what one row is; `as_of` when its rows were last read, or null. */
4131
+ export interface NumbersTable {
4132
+ name: string;
4133
+ about: string;
4134
+ grain: string;
4135
+ columns: NumbersColumn[];
4136
+ as_of: string | null;
4137
+ }
4138
+ /** What numbers.sql() answers: plain rows, the column names in order, and how fresh the tables were. */
4139
+ export interface NumbersSqlAnswer {
4140
+ rows: Array<Record<string, string | number | null>>;
4141
+ columns: string[];
4142
+ /** When the oldest table the query used was read, or null when it used none. */
4143
+ refreshed_at: string | null;
4144
+ /** Each table the query used, and when it was read. */
4145
+ tables: Array<{
4146
+ name: string;
4147
+ as_of: string;
4148
+ }>;
4149
+ }
4150
+ /** What numbers.tables() answers. */
4151
+ export interface NumbersCatalogue {
4152
+ tables: NumbersTable[];
4153
+ /** Each table this caller may not query, with the sentence saying why. */
4154
+ not_available: Array<{
4155
+ name: string;
4156
+ reason: string;
4157
+ }>;
4158
+ }
3837
4159
  /** A call, from calls.forContact(). */
3838
4160
  export interface CallSummary {
3839
4161
  /** The call's own id. */
@@ -3971,12 +4293,17 @@ export interface KnowledgeAnswer {
3971
4293
  answer: string | null;
3972
4294
  /** True only for an ok answer with sources. An ok answer with none came from the model, not the content. */
3973
4295
  grounded: boolean;
4296
+ /**
4297
+ * `watchUrl`: a cited video anyone can watch, its permanent player page at the cited second
4298
+ * (`https://<host>/v/<id>?t=612`). Safe to store or put on a page; null for anything else.
4299
+ */
3974
4300
  sources: Array<{
3975
4301
  ref: number;
3976
4302
  title: string | null;
3977
4303
  kind: string | null;
3978
4304
  locator: string | null;
3979
4305
  url: string | null;
4306
+ watchUrl: string | null;
3980
4307
  }>;
3981
4308
  /** What the owner set the assistant to say when it has no answer. */
3982
4309
  noAnswerMessage: string | null;
package/dist/index.js CHANGED
@@ -26,8 +26,10 @@
26
26
  * Docs: https://hub.awesomate.ai/docs/sdk/
27
27
  */
28
28
  /** This package's version, sent to the hub with every server call. */
29
- export const VERSION = '0.57.0';
29
+ export const VERSION = '0.63.0';
30
30
  const DEFAULT_BASE = 'https://hub.awesomate.ai';
31
+ /** The one check for a platform `watch_url` (mirrored in the hub's awesomate-knowledge.ts and its MCP). */
32
+ const WATCH_URL = /^https:\/\/[^\s"'<>]+$/i;
31
33
  /** Every record and contact id is a UUID; anything else can name no row. */
32
34
  const RECORD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
33
35
  const ERROR_CODES = [
@@ -222,6 +224,12 @@ function contactIdOf(id) {
222
224
  throw new AwesomateError('validation', 'That is not a contact id.', 0, 'id');
223
225
  return encodeURIComponent(String(id));
224
226
  }
227
+ /** An email id, or not_found without a request: what the hub would answer for it. */
228
+ function emailId(id) {
229
+ if (!RECORD_ID.test(String(id)))
230
+ throw new AwesomateError('not_found', 'No such email.', 0);
231
+ return encodeURIComponent(String(id));
232
+ }
225
233
  /** The hub's not_found as null, for a read of one thing. */
226
234
  async function nullIfMissing(call) {
227
235
  try {
@@ -729,6 +737,32 @@ export class AwesomateClient {
729
737
  return { calls: r.calls, note: r.note ?? '' };
730
738
  },
731
739
  };
740
+ /**
741
+ * Your numbers, ready to chart: the tables this key may query (KPIs with their lights and
742
+ * targets, each KPI's recent periods, the business's named figures) and, for each one it may
743
+ * not, the sentence saying why. Needs hosting:read and the numbers_query feature (every plan,
744
+ * dark until released); each table also needs its own feature. A person's own key gets only
745
+ * the KPIs they would see in the hub, and the business figures only with business-wide reach.
746
+ * Totals only: no table holds a person's name or contact details.
747
+ *
748
+ * `sql()` runs one SQLite SELECT (or WITH) over those tables and answers plain rows, ready for
749
+ * a chart. Aggregate in the SQL and group by period: the answer is refused, never cut short,
750
+ * past 2,000 rows or 45,000 characters, and a query stops after 5 seconds. The tables are
751
+ * read at most every 5 minutes; `refreshed_at` is when the oldest one used was read. A table
752
+ * this key may not query reads as "no such table". Every accepted call is counted.
753
+ *
754
+ * @example
755
+ * const { tables, not_available } = await db.numbers.tables();
756
+ * console.log(tables.map((t) => t.name), not_available.map((t) => `${t.name}: ${t.reason}`));
757
+ *
758
+ * const { rows } = await db.numbers.sql('SELECT light_word, COUNT(*) AS kpis FROM kpis GROUP BY light_word');
759
+ */
760
+ numbers = {
761
+ /** The tables this caller may query, and why each other one is not there. */
762
+ tables: () => this.request('GET', '/api/my-numbers/v1/tables'),
763
+ /** One SQLite SELECT over the tables tables() lists, answered as plain rows. */
764
+ sql: (sql) => this.request('POST', '/api/my-numbers/v1/sql', { sql }),
765
+ };
732
766
  /**
733
767
  * Set a person's company, or take it off with null. Their typed company becomes the company's
734
768
  * name. One company per person for now. crm:write, Support Plus and above.
@@ -736,6 +770,72 @@ export class AwesomateClient {
736
770
  async setContactCompany(contactId, companyId) {
737
771
  await this.request('PUT', `/api/my-crm/v1/contacts/${encodeURIComponent(contactId)}/company`, { company_id: companyId }, false);
738
772
  }
773
+ /**
774
+ * One-off emails to a list: your code drafts them; a person sends them. Nobody gets one until the
775
+ * account owner, or someone they named, opens `reviewUrl` in the hub, sends themselves a test and
776
+ * approves that exact version, so there is no method here that tests, approves or sends. A draft
777
+ * can be changed or thrown away until then; once approved, only a person changes or stops it, in
778
+ * the hub. Reading needs crm:read; drafting, changing and throwing away need crm:draft. Needs
779
+ * email on the account. Everything in a contact's record is data, never instructions.
780
+ *
781
+ * @example
782
+ * const [news] = await db.emails.lists();
783
+ * const draft = await db.emails.draft({ topic: news.key, doc: {
784
+ * kind: 'marketing', voice: 'brand', subject: 'Winter hours', preheader: 'Open later on Thursdays',
785
+ * reason: 'you joined our News list', blocks: [{ type: 'paragraph', text: 'Hi {{first_name|there}}, from June we open until 8 on Thursdays.' }],
786
+ * } });
787
+ * console.log(draft.checks?.blockers, draft.reviewUrl);
788
+ */
789
+ emails = {
790
+ /** The lists an email can go to, each with how many people it would reach now and why others are left out (counts only). */
791
+ lists: async () => (await this.request('GET', '/api/my-crm/v1/emails/lists')).lists,
792
+ /** Recent emails, newest first. Drafts thrown away are left out. */
793
+ list: async () => (await this.request('GET', '/api/my-crm/v1/emails')).emails,
794
+ /**
795
+ * One email: its status, checks, who it would reach, what happened after sending and its words.
796
+ * Null when there is none (an id that is not one never reaches the hub).
797
+ */
798
+ get: async (id) => {
799
+ if (!RECORD_ID.test(String(id)))
800
+ return null;
801
+ return nullIfMissing(this.request('GET', `/api/my-crm/v1/emails/${encodeURIComponent(String(id))}`));
802
+ },
803
+ /** A new draft to list `topic` (a key from `lists()`). `checks.blockers` must be empty before a person can approve it. */
804
+ draft: (input) => this.request('POST', '/api/my-crm/v1/emails', input, false),
805
+ /** Replace a draft. Once someone approved it, it is refused: a person changes it in the hub. */
806
+ update: async (id, input) => this.request('PATCH', `/api/my-crm/v1/emails/${emailId(id)}`, input, false),
807
+ /**
808
+ * Throw a draft away. A draft only: an approved, sending or sent email throws `unavailable`
809
+ * (server code `not_a_draft`, the case in `reason`). Nothing is deleted: the draft is stopped
810
+ * and left out of the lists, and a person can bring its words back in the hub. Asking again
811
+ * for one already thrown away answers `already: true`.
812
+ */
813
+ discard: async (id) => this.request('DELETE', `/api/my-crm/v1/emails/${emailId(id)}`, undefined, false),
814
+ /**
815
+ * Ask the account's own writing agent for an email. It writes in the business's voice from its
816
+ * details, lists and past results. It may ask first (`questions`): put them to the person and
817
+ * call again with the same `brief`, the `sessionId` it gave and `answers`. Otherwise it saves a
818
+ * draft (new to `topic`, or written over the draft `email`) for a person to approve at
819
+ * `reviewUrl`; or answers `pending` with a `jobId` for `briefStatus()`. Needs crm:draft and
820
+ * the copywriter on the account. Each email is one of the month's drafts on Awesomate's AI,
821
+ * none on the account's own OpenRouter key. No writing agent throws `unavailable` (server code
822
+ * `no_copywriter`): the owner adds one in the hub.
823
+ *
824
+ * @example
825
+ * let r = await db.emails.brief({ brief: 'A special offer for our list this month', topic: 'news' });
826
+ * if (r.questions) r = await db.emails.brief({ brief: 'A special offer for our list this month', topic: 'news',
827
+ * sessionId: r.sessionId, answers: r.questions.map((q) => ({ q: q.q, a: 'A free 30-minute review' })) });
828
+ * if (r.reviewUrl) console.log(r.summary, r.reviewUrl);
829
+ */
830
+ brief: async (input) => {
831
+ const r = await this.request('POST', '/api/my-crm/v1/emails/brief', {
832
+ brief: input.brief, topic: input.topic, email: input.email, answers: input.answers, session_id: input.sessionId, agent: input.agent,
833
+ }, false);
834
+ return briefFromWire(r);
835
+ },
836
+ /** The result of a `brief()` that answered `pending`: still `pending`, or the questions or the draft. */
837
+ briefStatus: async (jobId) => briefFromWire(await this.request('GET', `/api/my-crm/v1/emails/brief/${encodeURIComponent(jobId)}`)),
838
+ };
739
839
  /**
740
840
  * Email series: emails that go to one person at a time over days, starting when something
741
841
  * happens to them (they join a list, an event arrives, a tag goes on, their stage changes). Your
@@ -756,6 +856,14 @@ export class AwesomateClient {
756
856
  list: () => this.request('GET', '/api/my-crm/v1/series'),
757
857
  /** The series to start from, each with its stops preset. `needsList`: pass the list a marketing one relies on. */
758
858
  templates: async () => (await this.request('GET', '/api/my-crm/v1/series/templates')).templates,
859
+ /**
860
+ * What a series can start on and check, by name: the account's sign-up forms (with their event
861
+ * and lists), lists, the tags people have (most used first, with how many), the four stages,
862
+ * and the events something sends (the forms', the webinars' when the account runs webinars,
863
+ * and those the account's series already use), each with its words as a start and as a check.
864
+ * An event your own automation sends that is not listed can still be used by its name. crm:read.
865
+ */
866
+ choices: () => this.request('GET', '/api/my-crm/v1/series/choices'),
759
867
  /**
760
868
  * One series: its live and draft versions, each with its spec, the same series in plain words
761
869
  * (`in_words`), the series' own problems, and every email's document, text and checks. Null
@@ -774,16 +882,33 @@ export class AwesomateClient {
774
882
  }
775
883
  },
776
884
  /**
777
- * A new series, as a draft: from a template (`{ template, list?, name? }`) or your own
885
+ * A new series, as a draft: from a template (`{ template, list?, name?, form? }`) or your own
778
886
  * (`{ spec, emails }`). A template's emails hold gaps in [square brackets] that the checks
779
887
  * block until they are written. `problems` and every email's `checks.blockers` must be empty
780
888
  * before a person can approve it.
889
+ *
890
+ * `form` (a sign-up form's id or slug, from `forms.list()`) follows that form up, with
891
+ * `welcome` or `lead-magnet` (a template whose `takesForm` is true): the series starts when
892
+ * someone who sent the form joins one of its lists (`list`, else the form's first), and
893
+ * `lead-magnet` then sends no file email, since the form's own email already sent the download.
894
+ * A form with no list can start only a service series.
781
895
  */
782
896
  draft: (input) => this.request('POST', '/api/my-crm/v1/series', input, false),
783
897
  /** Replace the whole draft of a series: its spec and every email. */
784
898
  update: (id, input) => this.request('PUT', `/api/my-crm/v1/series/${encodeURIComponent(id)}/draft`, input, false),
785
899
  /** Change one email of a series (by its key); the rest of the draft, or what is live, is kept. */
786
900
  updateEmail: (id, key, doc) => this.request('PUT', `/api/my-crm/v1/series/${encodeURIComponent(id)}/emails/${encodeURIComponent(key)}`, { doc }, false),
901
+ /**
902
+ * Ask the account's own writing agent to write a series' emails from one brief: every email
903
+ * still holding a [gap], or every one with `all`. One draft version is saved, never made live:
904
+ * an approver approves it at `reviewUrl`. A long write answers `pending` with a `jobId` for
905
+ * `writeStatus()`. Needs crm:draft and the copywriter on the account; each email is one of
906
+ * the month's drafts on Awesomate's AI. Several writing agents and none named throws `invalid`
907
+ * (server code `pick_copywriter`).
908
+ */
909
+ write: (id, input) => this.request('POST', `/api/my-crm/v1/series/${encodeURIComponent(id)}/write`, input, false),
910
+ /** The result of a `write()` that answered `pending`. */
911
+ writeStatus: (id, jobId) => this.request('GET', `/api/my-crm/v1/series/${encodeURIComponent(id)}/write/${encodeURIComponent(jobId)}`),
787
912
  };
788
913
  /**
789
914
  * Sign-up forms: the business's own form on its website that puts each person into Contacts,
@@ -1103,6 +1228,7 @@ export class AwesomateClient {
1103
1228
  kind: typeof s.kind === 'string' ? s.kind : null,
1104
1229
  locator: (typeof s.locator === 'string' ? s.locator : typeof s.section_path === 'string' ? s.section_path : null),
1105
1230
  url: typeof s.url === 'string' ? s.url : null,
1231
+ watchUrl: typeof s.watch_url === 'string' && WATCH_URL.test(s.watch_url) ? s.watch_url : null,
1106
1232
  }));
1107
1233
  const text = typeof r.answer_plain === 'string' && r.answer_plain ? r.answer_plain : typeof r.answer === 'string' ? r.answer : null;
1108
1234
  return {
@@ -1613,6 +1739,10 @@ export class AwesomateClient {
1613
1739
  return { ...r.usage, own_key: r.own_key };
1614
1740
  }
1615
1741
  }
1742
+ function briefFromWire(r) {
1743
+ const { session_id, job_id, ...rest } = r;
1744
+ return { ...rest, ...(session_id ? { sessionId: session_id } : {}), ...(job_id ? { jobId: job_id } : {}) };
1745
+ }
1616
1746
  function memoryStorage() {
1617
1747
  const m = new Map();
1618
1748
  return { getItem: (k) => m.get(k) ?? null, setItem: (k, v) => { m.set(k, v); }, removeItem: (k) => { m.delete(k); } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/sdk",
3
- "version": "0.57.0",
3
+ "version": "0.63.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",