@saplingai/sapling-js 1.0.39 → 1.0.52

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/sapling/client.js CHANGED
@@ -1,6 +1,61 @@
1
1
  import axios from "axios";
2
2
  import { v4 as uuidv4 } from 'uuid';
3
3
 
4
+ // Value types an extract() field may declare; 'string' is the default.
5
+ const EXTRACT_FIELD_TYPES = ['string', 'number', 'integer', 'boolean', 'date', 'list'];
6
+ // Registers translate() accepts; the API rejects anything else with a 400.
7
+ const TRANSLATE_FORMALITIES = ['formal', 'informal'];
8
+ const SUMMARIZE_LENGTHS = ['short', 'medium', 'long'];
9
+ // Reading levels simplify() accepts; the API rejects anything else with a 400.
10
+ const SIMPLIFY_READING_LEVELS = ['plain', 'elementary', 'middle_school', 'high_school'];
11
+
12
+ // Shared by styleguide() and saveStyleguideRuleset(). Shape/type checks only —
13
+ // size limits (rule/description length) are server configuration and enforced
14
+ // there with a clear 400 (see classify()).
15
+ const validateStyleguideRules = (rules, method) => {
16
+ const isValidRule = (rule) => {
17
+ if (typeof rule === 'string') {
18
+ return rule.trim().length > 0;
19
+ }
20
+ if (rule == null || typeof rule !== 'object' || Array.isArray(rule)) {
21
+ return false;
22
+ }
23
+ if (typeof rule.name !== 'string' || rule.name.trim().length === 0) {
24
+ return false;
25
+ }
26
+ if (rule.description !== undefined && typeof rule.description !== 'string') {
27
+ return false;
28
+ }
29
+ // Reject unknown keys so a typo like `{ name, descrption: '...' }` fails
30
+ // here rather than being forwarded to the API as a malformed rule (the
31
+ // classify()/extract() convention; the MCP object schema is `.strict()`).
32
+ const allowed = new Set(['name', 'description']);
33
+ if (Object.keys(rule).some((key) => !allowed.has(key))) {
34
+ return false;
35
+ }
36
+ return true;
37
+ };
38
+
39
+ if (
40
+ !Array.isArray(rules)
41
+ || rules.length < 1
42
+ || rules.length > 20
43
+ // Array.from() materializes holes in sparse arrays as `undefined` (which
44
+ // isValidRule rejects); a bare `.every()` would skip them and let a hole
45
+ // through to be serialized as `null`.
46
+ || !Array.from(rules).every(isValidRule)
47
+ ) {
48
+ throw new TypeError(`${method}: rules must be an array of 1 to 20 non-empty strings or `
49
+ + '{name, description?} objects (string name, optional string description).');
50
+ }
51
+
52
+ const names = rules.map(
53
+ (rule) => (typeof rule === 'string' ? rule : rule.name).trim().toLowerCase());
54
+ if (names.length !== new Set(names).size) {
55
+ throw new TypeError(`${method}: rule names must be unique (case-insensitive).`);
56
+ }
57
+ };
58
+
4
59
  export class Client {
5
60
  constructor(apiKey, options = {}) {
6
61
  this.apiKey = apiKey;
@@ -13,7 +68,7 @@ export class Client {
13
68
  }
14
69
 
15
70
  axiosPromise(url, method, data) {
16
- return new Promise((resolve) => {
71
+ return new Promise((resolve, reject) => {
17
72
  axios({
18
73
  url,
19
74
  method,
@@ -22,7 +77,8 @@ export class Client {
22
77
  })
23
78
  .then(function (response) {
24
79
  resolve(response);
25
- });
80
+ })
81
+ .catch(reject);
26
82
  });
27
83
  }
28
84
 
@@ -56,6 +112,11 @@ export class Client {
56
112
  data.advanced_edits = options.advancedEdits;
57
113
  }
58
114
 
115
+ // Omitted unless set; an explicit null is forwarded so the API rejects it.
116
+ if (options.includeErrorCategories !== undefined) {
117
+ data.include_error_categories = options.includeErrorCategories;
118
+ }
119
+
59
120
  if (userDetails) {
60
121
  const { userId, anonUser } = userDetails;
61
122
  data.user_id = userId;
@@ -176,11 +237,304 @@ export class Client {
176
237
  return this.axiosPromise(url, 'post', data);
177
238
  }
178
239
 
240
+ /**
241
+ * Structured data extraction: pull caller-named fields out of unstructured text.
242
+ *
243
+ * `classify()` answers "what kind of text is this"; `extract()` answers "what's in it".
244
+ * Pass 1–20 fields — plain names, or `{name, type, description, required}` objects to
245
+ * declare the value type and sharpen ambiguous fields. Returns `data` (a
246
+ * `{field name: value}` map with every requested field present and `null` where the text
247
+ * didn't state it), `fields` (one entry per requested field, in request order, each with
248
+ * the verbatim `evidence` span the value came from) and `missing` (the names the text
249
+ * didn't yield).
250
+ *
251
+ * Values are coerced to the declared type server-side and must be grounded in a quote
252
+ * from the text, so this is extraction, not inference: a value that isn't stated is
253
+ * reported missing rather than guessed. Finding nothing is a normal 200.
254
+ *
255
+ * @param {string|string[]} text - The document to extract from, plain text or HTML (the
256
+ * API caps it at 10,000 characters). Pass an array of 1–10 strings to extract from a
257
+ * batch in one request (sent as the API's `texts` parameter; combined length capped at
258
+ * 10,000 chars) — the response is then `{results: [...]}` with one
259
+ * single-response-shaped entry per input, in order.
260
+ * @param {Array<string|{name: string, type?: string, description?: string, required?: boolean}>} fields -
261
+ * The fields to pull out (1–20): field names, or objects. `type` is one of 'string'
262
+ * (default), 'number', 'integer', 'boolean', 'date' (returned as YYYY-MM-DD) or 'list'.
263
+ * Names must be unique case-insensitively; the API caps names at 50 characters and
264
+ * descriptions at 200.
265
+ * @param {Object} [options]
266
+ * @param {string} [options.context] - Optional guidance (the API caps it at 500
267
+ * characters): what the document is and how to read ambiguous fields, e.g.
268
+ * 'A vendor invoice'.
269
+ * @returns {Promise<AxiosResponse>}
270
+ */
271
+ extract(text, fields, options = {}) {
272
+ const url = `${this.urlEndpoint}extract`;
273
+
274
+ // Shape/type checks only. Size limits (text length, field name/description/
275
+ // context length) are server configuration and enforced there with a clear
276
+ // 400 — mirroring them here would drift the day they change (and JS
277
+ // `length` counts UTF-16 units, not the characters the API counts).
278
+ // An array of texts is the batch form (sent as `texts`, one result per
279
+ // item); the 1–10 item bound is structural like the field count below.
280
+ const isBatch = Array.isArray(text);
281
+ if (isBatch) {
282
+ // for...of (not text.every) so holes in a sparse array are visited as
283
+ // undefined and rejected, rather than skipped.
284
+ let ok = text.length >= 1 && text.length <= 10;
285
+ for (const item of text) {
286
+ ok = ok && typeof item === 'string' && item.trim().length > 0;
287
+ }
288
+ if (!ok) {
289
+ throw new TypeError('extract: texts must be an array of 1 to 10 non-empty strings.');
290
+ }
291
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
292
+ throw new TypeError('extract: text must be a non-empty string.');
293
+ }
294
+
295
+ const isValidField = (field) => {
296
+ if (typeof field === 'string') {
297
+ return field.trim().length > 0;
298
+ }
299
+ if (field == null || typeof field !== 'object' || Array.isArray(field)) {
300
+ return false;
301
+ }
302
+ if (typeof field.name !== 'string' || field.name.trim().length === 0) {
303
+ return false;
304
+ }
305
+ if (field.type !== undefined && !EXTRACT_FIELD_TYPES.includes(field.type)) {
306
+ return false;
307
+ }
308
+ if (field.description !== undefined && typeof field.description !== 'string') {
309
+ return false;
310
+ }
311
+ if (field.required !== undefined && typeof field.required !== 'boolean') {
312
+ return false;
313
+ }
314
+ // Reject unknown keys so a typo like `{ name, typ: 'number' }` fails here
315
+ // rather than being forwarded to the API as a malformed field (the MCP
316
+ // schema is `.strict()` for the same reason).
317
+ const allowed = new Set(['name', 'type', 'description', 'required']);
318
+ if (Object.keys(field).some((key) => !allowed.has(key))) {
319
+ return false;
320
+ }
321
+ return true;
322
+ };
323
+
324
+ if (
325
+ !Array.isArray(fields)
326
+ || fields.length < 1
327
+ || fields.length > 20
328
+ // Array.from() materializes holes in sparse arrays as `undefined` (which
329
+ // isValidField rejects); a bare `.every()` would skip them and let a hole
330
+ // through to be serialized as `null`.
331
+ || !Array.from(fields).every(isValidField)
332
+ ) {
333
+ throw new TypeError('extract: fields must be an array of 1 to 20 non-empty strings or '
334
+ + '{name, type?, description?, required?} objects (string name, type one of '
335
+ + `${EXTRACT_FIELD_TYPES.join(', ')}, optional string description, optional boolean required).`);
336
+ }
337
+
338
+ const names = fields.map((field) => (typeof field === 'string' ? field : field.name).toLowerCase());
339
+ if (names.length !== new Set(names).size) {
340
+ throw new TypeError('extract: field names must be unique (case-insensitive).');
341
+ }
342
+
343
+ let data = {
344
+ ...(isBatch ? { texts: text } : { text }),
345
+ fields,
346
+ key: this.apiKey,
347
+ }
348
+
349
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
350
+ // type rather than silently treated as "omitted", matching how a null
351
+ // `description` is rejected above.
352
+ if (options?.context !== undefined) {
353
+ if (typeof options.context !== 'string') {
354
+ throw new TypeError('extract: context must be a string.');
355
+ }
356
+ data.context = options.context;
357
+ }
358
+
359
+ return this.axiosPromise(url, 'post', data);
360
+ }
361
+
362
+ /**
363
+ * Translate text into a target language with an LLM.
364
+ *
365
+ * Line breaks, markup and placeholders are preserved in place, and the response
366
+ * reports the language the text was actually written in (detected — never assumed
367
+ * from the hint): `translation`, `source_lang`/`source_lang_name` (`'und'`/`null`
368
+ * when undetermined) and the normalized `target_lang`/`target_lang_name`.
369
+ *
370
+ * @param {string|string[]} text - The text to translate (the API caps it at
371
+ * 5,000 characters). Markup and whitespace are preserved, not stripped.
372
+ * Pass an array of 1-10 strings to translate a batch in one request (sent
373
+ * as the API's `texts` parameter; combined length up to 5,000 characters,
374
+ * same target/source/formality applied to every item). A batch response is
375
+ * `{results: [...]}` with one single-response-shaped entry per input, in
376
+ * order.
377
+ * @param {string} targetLang - Language to translate into: an ISO 639 code
378
+ * ('fr', 'zh-TW') or an English language name ('French'). Region variants
379
+ * zh-CN/zh-TW/zh-HK, pt-BR/pt-PT, en-GB/en-US, fr-CA and es-419 are supported
380
+ * as targets.
381
+ * @param {Object} [options]
382
+ * @param {string} [options.source_lang] - Optional hint for the source language,
383
+ * same forms as targetLang. The text itself wins if they disagree.
384
+ * @param {string} [options.formality] - Optional register: 'formal'
385
+ * (vous/Sie/usted) or 'informal' (tu/du/tú). Omit for the model default.
386
+ * @returns {Promise<AxiosResponse>}
387
+ */
388
+ translate(text, targetLang, options = {}) {
389
+ const url = `${this.urlEndpoint}translate`;
390
+
391
+ // Shape/type checks only; the language vocabulary and length caps are
392
+ // server configuration enforced there with a clear 400 (mirroring the
393
+ // 176-language table here would drift the day it changes).
394
+ // An array of texts is the batch form (SAP-398, sent as `texts`, one
395
+ // result per item); the 1–10 item bound is structural like safety's.
396
+ const isBatch = Array.isArray(text);
397
+ if (isBatch) {
398
+ // for...of (not text.every) so holes in a sparse array are visited as
399
+ // undefined and rejected, rather than skipped.
400
+ let ok = text.length >= 1 && text.length <= 10;
401
+ if (ok) {
402
+ for (const item of text) {
403
+ if (typeof item !== 'string' || item.trim().length === 0) {
404
+ ok = false;
405
+ break;
406
+ }
407
+ }
408
+ }
409
+ if (!ok) {
410
+ throw new TypeError('translate: texts must be an array of 1 to 10 non-empty strings.');
411
+ }
412
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
413
+ throw new TypeError('translate: text must be a non-empty string.');
414
+ }
415
+ if (typeof targetLang !== 'string' || targetLang.trim().length === 0) {
416
+ throw new TypeError('translate: targetLang must be a non-empty string (an ISO 639 '
417
+ + 'code like "fr" or an English language name like "French").');
418
+ }
419
+
420
+ let data = {
421
+ ...(isBatch ? { texts: text } : { text }),
422
+ target_lang: targetLang,
423
+ key: this.apiKey,
424
+ }
425
+
426
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
427
+ // bad type rather than silently treated as "omitted" (same as extract's
428
+ // context handling).
429
+ if (options?.source_lang !== undefined) {
430
+ if (typeof options.source_lang !== 'string' || options.source_lang.trim().length === 0) {
431
+ throw new TypeError('translate: source_lang must be a non-empty string.');
432
+ }
433
+ data.source_lang = options.source_lang;
434
+ }
435
+ if (options?.formality !== undefined) {
436
+ if (!TRANSLATE_FORMALITIES.includes(options.formality)) {
437
+ throw new TypeError('translate: formality must be one of '
438
+ + `${TRANSLATE_FORMALITIES.join(', ')}.`);
439
+ }
440
+ data.formality = options.formality;
441
+ }
442
+
443
+ return this.axiosPromise(url, 'post', data);
444
+ }
445
+
446
+ /**
447
+ * Summarize a document, webpage or email thread into a prose summary plus
448
+ * key points.
449
+ *
450
+ * Returns `summary` (the prose summary), `key_points` (an array of strings),
451
+ * `length` (the option the summary was generated with) and `result`
452
+ * (mirrors `summary` — the endpoint's original response key, kept for
453
+ * backward compatibility).
454
+ *
455
+ * @param {string|string[]} text - The document to summarize, plain text or
456
+ * HTML (tags are stripped; the API caps it at 20,000 characters). Pass an
457
+ * array of 1-10 strings to summarize a batch in one request (sent as the
458
+ * API's `texts` parameter; combined length up to 20,000 characters, same
459
+ * length option applied to every item). A batch response is
460
+ * `{results: [...]}` with one single-response-shaped entry per input, in
461
+ * order.
462
+ * @param {Object} [options]
463
+ * @param {string} [options.length] - Optional summary length: 'short',
464
+ * 'medium' (the default) or 'long'.
465
+ * @returns {Promise<AxiosResponse>}
466
+ */
467
+ summarize(text, options = {}) {
468
+ const url = `${this.urlEndpoint}summarize`;
469
+
470
+ // An array of texts is the batch form (SAP-399, sent as `texts`, one
471
+ // result per item); the 1–10 item bound is structural like translate's.
472
+ const isBatch = Array.isArray(text);
473
+ if (isBatch) {
474
+ // for...of (not text.every) so holes in a sparse array are visited as
475
+ // undefined and rejected, rather than skipped.
476
+ let ok = text.length >= 1 && text.length <= 10;
477
+ if (ok) {
478
+ for (const item of text) {
479
+ if (typeof item !== 'string' || item.trim().length === 0) {
480
+ ok = false;
481
+ break;
482
+ }
483
+ }
484
+ }
485
+ if (!ok) {
486
+ throw new TypeError('summarize: texts must be an array of 1 to 10 non-empty strings.');
487
+ }
488
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
489
+ throw new TypeError('summarize: text must be a non-empty string.');
490
+ }
491
+
492
+ let data = {
493
+ ...(isBatch ? { texts: text } : { text }),
494
+ key: this.apiKey,
495
+ }
496
+
497
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
498
+ // bad type rather than silently treated as "omitted" (same as translate's
499
+ // formality handling).
500
+ if (options?.length !== undefined) {
501
+ if (!SUMMARIZE_LENGTHS.includes(options.length)) {
502
+ throw new TypeError('summarize: length must be one of '
503
+ + `${SUMMARIZE_LENGTHS.join(', ')}.`);
504
+ }
505
+ data.length = options.length;
506
+ }
507
+
508
+ return this.axiosPromise(url, 'post', data);
509
+ }
510
+
179
511
  langdetect(text, options = {}) {
180
512
  const url = `${this.urlEndpoint}langdetect`;
181
513
 
514
+ // An array of texts is the batch form (SAP-397, sent as `texts`, one
515
+ // result per item); the 1–10 item bound is structural like classify's.
516
+ // The single (string) form is passed through unvalidated, as it always
517
+ // has been — the server answers a clear 400.
518
+ const isBatch = Array.isArray(text);
519
+ if (isBatch) {
520
+ // for...of (not text.every) so holes in a sparse array are visited as
521
+ // undefined and rejected, rather than skipped.
522
+ let ok = text.length >= 1 && text.length <= 10;
523
+ if (ok) {
524
+ for (const item of text) {
525
+ if (typeof item !== 'string' || item.trim().length === 0) {
526
+ ok = false;
527
+ break;
528
+ }
529
+ }
530
+ }
531
+ if (!ok) {
532
+ throw new TypeError('langdetect: texts must be an array of 1 to 10 non-empty strings.');
533
+ }
534
+ }
535
+
182
536
  let data = {
183
- text,
537
+ ...(isBatch ? { texts: text } : { text }),
184
538
  key: this.apiKey,
185
539
  }
186
540
 
@@ -194,11 +548,318 @@ export class Client {
194
548
  return this.axiosPromise(url, 'post', data);
195
549
  }
196
550
 
551
+ /**
552
+ * Zero-shot text classification: assign one (or several) of your own labels to a text.
553
+ *
554
+ * No training data needed — pass 2–20 candidate labels (plain names, or
555
+ * `{name, description}` objects; a short description per label sharpens borderline
556
+ * decisions). Returns `label` (best match, always one of your labels), `labels`
557
+ * (single-label: `[label]`; multi-label: every label with `score >= threshold`, possibly
558
+ * empty), `scores` (one `{label, score}` per input label, descending), a one-sentence
559
+ * `rationale` and the echoed `multi_label` mode.
560
+ *
561
+ * @param {string|string[]} text - Text to classify, plain text or HTML (the API caps it
562
+ * at 10,000 chars). Pass an array of 1–10 strings to classify a batch in one request
563
+ * (sent as the API's `texts` parameter; combined length capped at 10,000 chars) — the
564
+ * response is then `{results: [...]}` with one single-response-shaped entry per input,
565
+ * in order.
566
+ * @param {Array<string|{name: string, description?: string}>} labels - Candidate labels
567
+ * (2–20). Names unique case-insensitively; the API caps names at 50 and descriptions at 200 chars.
568
+ * @param {Object} [options]
569
+ * @param {boolean} [options.multiLabel] - true: zero, one or several labels may apply
570
+ * (independent 0–1 scores). Defaults to false: exactly one label (scores sum to ~1).
571
+ * @param {number} [options.threshold] - Multi-label cut-off on the per-label score (0–1,
572
+ * default 0.5). Ignored in single-label mode.
573
+ * @param {string} [options.context] - Optional guidance (max 500 chars): what the texts
574
+ * are and how to decide borderline cases, e.g. 'Support tickets for a SaaS billing product'.
575
+ * @returns {Promise<AxiosResponse>}
576
+ */
577
+ classify(text, labels, options = {}) {
578
+ const url = `${this.urlEndpoint}classify`;
579
+
580
+ // Shape/type checks only. Size limits (text length, label/description/
581
+ // context length) are server configuration and enforced there with a
582
+ // clear 400 — mirroring them here would drift the day they change (and
583
+ // JS `length` counts UTF-16 units, not the characters the API counts).
584
+ // An array of texts is the batch form (sent as `texts`, one result per
585
+ // item); the 1–10 item bound is structural like the label count below.
586
+ const isBatch = Array.isArray(text);
587
+ if (isBatch) {
588
+ // for...of (not text.every) so holes in a sparse array are visited as
589
+ // undefined and rejected, rather than skipped.
590
+ let ok = text.length >= 1 && text.length <= 10;
591
+ for (const item of text) {
592
+ ok = ok && typeof item === 'string' && item.trim().length > 0;
593
+ }
594
+ if (!ok) {
595
+ throw new TypeError('classify: texts must be an array of 1 to 10 non-empty strings.');
596
+ }
597
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
598
+ throw new TypeError('classify: text must be a non-empty string.');
599
+ }
600
+
601
+ const isValidLabel = (label) => {
602
+ if (typeof label === 'string') {
603
+ return label.trim().length > 0;
604
+ }
605
+ if (label == null || typeof label !== 'object' || Array.isArray(label)) {
606
+ return false;
607
+ }
608
+ if (typeof label.name !== 'string' || label.name.trim().length === 0) {
609
+ return false;
610
+ }
611
+ if (label.description !== undefined && typeof label.description !== 'string') {
612
+ return false;
613
+ }
614
+ // Reject unknown keys so a typo like `{ name, descrption: '...' }` fails
615
+ // here rather than being forwarded to the API as a malformed label (the
616
+ // MCP object schema is `.strict()` and `extract()` does the same).
617
+ const allowed = new Set(['name', 'description']);
618
+ if (Object.keys(label).some((key) => !allowed.has(key))) {
619
+ return false;
620
+ }
621
+ return true;
622
+ };
623
+
624
+ if (
625
+ !Array.isArray(labels)
626
+ || labels.length < 2
627
+ || labels.length > 20
628
+ // Array.from() materializes holes in sparse arrays as `undefined` (which
629
+ // isValidLabel rejects); a bare `.every()` would skip them and let a hole
630
+ // through to be serialized as `null`.
631
+ || !Array.from(labels).every(isValidLabel)
632
+ ) {
633
+ throw new TypeError('classify: labels must be an array of 2 to 20 non-empty strings or '
634
+ + '{name, description?} objects (string name, optional string description).');
635
+ }
636
+
637
+ const names = labels.map((label) => (typeof label === 'string' ? label : label.name).toLowerCase());
638
+ if (names.length !== new Set(names).size) {
639
+ throw new TypeError('classify: label names must be unique (case-insensitive).');
640
+ }
641
+
642
+ let data = {
643
+ ...(isBatch ? { texts: text } : { text }),
644
+ labels,
645
+ key: this.apiKey,
646
+ }
647
+
648
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
649
+ // type rather than silently treated as "omitted", matching how a null
650
+ // `description` is rejected above.
651
+ if (options?.multiLabel !== undefined) {
652
+ if (typeof options.multiLabel !== 'boolean') {
653
+ throw new TypeError('classify: multiLabel must be a boolean.');
654
+ }
655
+ data.multi_label = options.multiLabel;
656
+ }
657
+ if (options?.threshold !== undefined) {
658
+ if (typeof options.threshold !== 'number' || !Number.isFinite(options.threshold)
659
+ || options.threshold < 0 || options.threshold > 1) {
660
+ throw new TypeError('classify: threshold must be a finite number between 0 and 1.');
661
+ }
662
+ data.threshold = options.threshold;
663
+ }
664
+ if (options?.context !== undefined) {
665
+ if (typeof options.context !== 'string') {
666
+ throw new TypeError('classify: context must be a string.');
667
+ }
668
+ data.context = options.context;
669
+ }
670
+
671
+ return this.axiosPromise(url, 'post', data);
672
+ }
673
+
674
+ /**
675
+ * SEO analysis and metadata generation for a page or article body (plain text or HTML).
676
+ *
677
+ * Always returns deterministic `stats` (word/sentence/paragraph counts, reading time,
678
+ * Flesch readability), `keywords` (count/density/in_first_100_words for each target
679
+ * keyword) and `top_terms` (most frequent unigrams/bigrams). By default also returns
680
+ * LLM-generated `suggestions` (titles, meta_descriptions, slug — null when no ASCII slug
681
+ * can be derived — and keywords) — pass
682
+ * `suggestions: false` for the free stats-only mode.
683
+ *
684
+ * @param {string} text - Page/article body, plain text or HTML (max 20,000 chars).
685
+ * @param {Object} [options]
686
+ * @param {string[]} [options.keywords] - Target keyword phrases, most important first
687
+ * (max 10, each 1–100 chars). Measured in the text and steered into the suggestions.
688
+ * @param {boolean} [options.suggestions] - Generate LLM suggestions. Defaults to true;
689
+ * false returns stats/keywords/top_terms only (unbilled).
690
+ * @param {string} [options.lang] - ISO 639-1 language code of the text (region subtag
691
+ * allowed, e.g. 'pt-BR'; or 'auto'). Defaults to 'en'. Only selects the readability
692
+ * formulas: supported for en/de/es/fr/it/nl/pl/ru, otherwise flesch_reading_ease /
693
+ * flesch_kincaid_grade are null.
694
+ * @returns {Promise<AxiosResponse>}
695
+ */
696
+ seo(text, options = {}) {
697
+ const url = `${this.urlEndpoint}seo`;
698
+
699
+ // Shape/type checks only. Size limits (text length, keyword count/length)
700
+ // are server configuration and enforced there with a clear 400 — mirroring
701
+ // them here would drift the day they change (and JS `length` counts UTF-16
702
+ // units, not the characters the API counts).
703
+ if (typeof text !== 'string' || text.trim().length === 0) {
704
+ throw new TypeError('seo: text must be a non-empty string.');
705
+ }
706
+
707
+ let data = {
708
+ text,
709
+ key: this.apiKey,
710
+ }
711
+
712
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
713
+ // type rather than silently treated as "omitted", matching classify().
714
+ if (options?.keywords !== undefined) {
715
+ if (!Array.isArray(options.keywords)
716
+ || !Array.from(options.keywords).every((k) => typeof k === 'string' && k.trim().length > 0)) {
717
+ throw new TypeError('seo: keywords must be an array of non-empty strings.');
718
+ }
719
+ data.keywords = options.keywords;
720
+ }
721
+ if (options?.suggestions !== undefined) {
722
+ if (typeof options.suggestions !== 'boolean') {
723
+ throw new TypeError('seo: suggestions must be a boolean.');
724
+ }
725
+ data.suggestions = options.suggestions;
726
+ }
727
+ if (options?.lang !== undefined) {
728
+ if (typeof options.lang !== 'string') {
729
+ throw new TypeError('seo: lang must be a string.');
730
+ }
731
+ data.lang = options.lang;
732
+ }
733
+
734
+ return this.axiosPromise(url, 'post', data);
735
+ }
736
+
737
+ quality(text, options = {}) {
738
+ const url = `${this.urlEndpoint}quality`;
739
+
740
+ // Shape/type checks only; size limits are server configuration (see seo()).
741
+ // An array of texts is the batch form (SAP-400, sent as `texts`, one
742
+ // result per item); the 1–10 item bound is structural like classify's.
743
+ const isBatch = Array.isArray(text);
744
+ if (isBatch) {
745
+ // for...of (not text.every) so holes in a sparse array are visited as
746
+ // undefined and rejected, rather than skipped.
747
+ let ok = text.length >= 1 && text.length <= 10;
748
+ if (ok) {
749
+ for (const item of text) {
750
+ if (typeof item !== 'string' || item.trim().length === 0) {
751
+ ok = false;
752
+ break;
753
+ }
754
+ }
755
+ }
756
+ if (!ok) {
757
+ throw new TypeError('quality: texts must be an array of 1 to 10 non-empty strings.');
758
+ }
759
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
760
+ throw new TypeError('quality: text must be a non-empty string.');
761
+ }
762
+
763
+ let data = {
764
+ ...(isBatch ? { texts: text } : { text }),
765
+ key: this.apiKey,
766
+ }
767
+
768
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
769
+ // type rather than silently treated as "omitted", matching classify()/seo().
770
+ if (options?.sentence_scores !== undefined) {
771
+ if (typeof options.sentence_scores !== 'boolean') {
772
+ throw new TypeError('quality: sentence_scores must be a boolean.');
773
+ }
774
+ data.sentence_scores = options.sentence_scores;
775
+ }
776
+ if (options?.rubric !== undefined) {
777
+ if (typeof options.rubric !== 'boolean') {
778
+ throw new TypeError('quality: rubric must be a boolean.');
779
+ }
780
+ data.rubric = options.rubric;
781
+ }
782
+
783
+ return this.axiosPromise(url, 'post', data);
784
+ }
785
+
786
+ safety(text, options = {}) {
787
+ const url = `${this.urlEndpoint}safety`;
788
+
789
+ // Shape/type checks only; size limits are server configuration (see seo()).
790
+ // An array of texts is the batch form (SAP-397, sent as `texts`, one
791
+ // result per item); the 1–10 item bound is structural like classify's.
792
+ const isBatch = Array.isArray(text);
793
+ if (isBatch) {
794
+ // for...of (not text.every) so holes in a sparse array are visited as
795
+ // undefined and rejected, rather than skipped.
796
+ let ok = text.length >= 1 && text.length <= 10;
797
+ if (ok) {
798
+ for (const item of text) {
799
+ if (typeof item !== 'string' || item.trim().length === 0) {
800
+ ok = false;
801
+ break;
802
+ }
803
+ }
804
+ }
805
+ if (!ok) {
806
+ throw new TypeError('safety: texts must be an array of 1 to 10 non-empty strings.');
807
+ }
808
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
809
+ throw new TypeError('safety: text must be a non-empty string.');
810
+ }
811
+
812
+ let data = {
813
+ ...(isBatch ? { texts: text } : { text }),
814
+ key: this.apiKey,
815
+ }
816
+
817
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
818
+ // type rather than silently treated as "omitted", matching classify().
819
+ if (options?.threshold !== undefined) {
820
+ if (typeof options.threshold !== 'number' || !Number.isFinite(options.threshold)
821
+ || options.threshold < 0 || options.threshold > 1) {
822
+ throw new TypeError('safety: threshold must be a finite number between 0 and 1.');
823
+ }
824
+ data.threshold = options.threshold;
825
+ }
826
+ if (options?.spans !== undefined) {
827
+ if (typeof options.spans !== 'boolean') {
828
+ throw new TypeError('safety: spans must be a boolean.');
829
+ }
830
+ data.spans = options.spans;
831
+ }
832
+
833
+ return this.axiosPromise(url, 'post', data);
834
+ }
835
+
197
836
  aidetect(text, options = {}) {
198
837
  const url = `${this.urlEndpoint}aidetect`;
199
838
 
839
+ // An array of texts is the batch form (SAP-403, sent as `texts`, one
840
+ // result per item); the 1–10 item bound is structural like classify's.
841
+ // The single form keeps its historical no-validation behavior (the
842
+ // server 400s clearly on a bad `text`).
843
+ const isBatch = Array.isArray(text);
844
+ if (isBatch) {
845
+ // for...of (not text.every) so holes in a sparse array are visited as
846
+ // undefined and rejected, rather than skipped.
847
+ let ok = text.length >= 1 && text.length <= 10;
848
+ if (ok) {
849
+ for (const item of text) {
850
+ if (typeof item !== 'string' || item.trim().length === 0) {
851
+ ok = false;
852
+ break;
853
+ }
854
+ }
855
+ }
856
+ if (!ok) {
857
+ throw new TypeError('aidetect: texts must be an array of 1 to 10 non-empty strings.');
858
+ }
859
+ }
860
+
200
861
  let data = {
201
- text,
862
+ ...(isBatch ? { texts: text } : { text }),
202
863
  key: this.apiKey,
203
864
  }
204
865
 
@@ -208,4 +869,398 @@ export class Client {
208
869
 
209
870
  return this.axiosPromise(url, 'post', data);
210
871
  }
872
+
873
+ /**
874
+ * Check a text for compliance with style rules you supply (house style,
875
+ * brand voice, editorial guidelines). Only the listed rules are enforced.
876
+ *
877
+ * Returns `violations` (one `{rule, text, start, end, note, suggestion}`
878
+ * entry per violation, most important first — `start`/`end` index the
879
+ * submitted text and are null when the passage cannot be located), `rules`
880
+ * (the checked rule names) and `compliant` (true when no violations were
881
+ * found). The text is checked exactly as submitted — markup is NOT
882
+ * stripped, so offsets always index the string you sent.
883
+ *
884
+ * @param {string|string[]} text - The text to check (the API caps it at
885
+ * 10,000 characters), or an array of 1–10 texts for the batch form (sent
886
+ * as the API's `texts` parameter; combined length up to 10,000
887
+ * characters, same rules applied to every item). A batch response is
888
+ * `{results: [...]}` with one single-response-shaped entry per input, in
889
+ * order, each item's violation offsets indexing that item's own text.
890
+ * @param {Array<string|{name: string, description?: string}>|string} rules
891
+ * - 1 to 20 style rules: a rule name (max 80 chars) or a
892
+ * `{name, description}` object whose optional description (max 400
893
+ * chars) carries the nuance the model follows. Names must be unique
894
+ * case-insensitively. OR a string: the name of a saved ruleset (see
895
+ * saveStyleguideRuleset()) to check against, matched case-insensitively
896
+ * within the API key's account.
897
+ * @returns {Promise<AxiosResponse>}
898
+ */
899
+ styleguide(text, rules) {
900
+ const url = `${this.urlEndpoint}styleguide`;
901
+
902
+ // Shape/type checks only. Size limits (text/rule/description length) are
903
+ // server configuration and enforced there with a clear 400 (see
904
+ // classify()).
905
+ // An array of texts is the batch form (SAP-412, sent as `texts`, one
906
+ // result per item); the 1–10 item bound is structural like classify's.
907
+ const isBatch = Array.isArray(text);
908
+ if (isBatch) {
909
+ // for...of (not text.every) so holes in a sparse array are visited as
910
+ // undefined and rejected, rather than skipped.
911
+ let ok = text.length >= 1 && text.length <= 10;
912
+ if (ok) {
913
+ for (const item of text) {
914
+ if (typeof item !== 'string' || item.trim().length === 0) {
915
+ ok = false;
916
+ break;
917
+ }
918
+ }
919
+ }
920
+ if (!ok) {
921
+ throw new TypeError('styleguide: texts must be an array of 1 to 10 non-empty strings.');
922
+ }
923
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
924
+ throw new TypeError('styleguide: text must be a non-empty string.');
925
+ }
926
+
927
+ // A string names a saved ruleset (see saveStyleguideRuleset()) and is
928
+ // sent as the API's `ruleset` parameter in place of inline `rules`.
929
+ const isSavedRuleset = typeof rules === 'string';
930
+ if (isSavedRuleset) {
931
+ if (rules.trim().length === 0) {
932
+ throw new TypeError('styleguide: ruleset name must be a non-empty string.');
933
+ }
934
+ } else {
935
+ validateStyleguideRules(rules, 'styleguide');
936
+ }
937
+
938
+ let data = {
939
+ ...(isBatch ? { texts: text } : { text }),
940
+ ...(isSavedRuleset ? { ruleset: rules } : { rules }),
941
+ key: this.apiKey,
942
+ }
943
+
944
+ return this.axiosPromise(url, 'post', data);
945
+ }
946
+
947
+ /**
948
+ * List the saved style-guide rulesets of this API key's account (its team,
949
+ * for team-scoped keys — a team's rulesets are shared by every caller of
950
+ * its keys). Requires a per-account API key.
951
+ *
952
+ * Returns `rulesets`: `{name, rules, created_at, updated_at}` entries whose
953
+ * `rules` are the normalized `{name, description}` objects the check
954
+ * endpoint applies.
955
+ *
956
+ * @returns {Promise<AxiosResponse>}
957
+ */
958
+ styleguideRulesets() {
959
+ const url = `${this.urlEndpoint}styleguide/rulesets`;
960
+ // Not axiosPromise: this is a GET and the key rides the query string.
961
+ return axios({
962
+ url,
963
+ method: 'get',
964
+ params: { key: this.apiKey },
965
+ timeout: this.timeout,
966
+ });
967
+ }
968
+
969
+ /**
970
+ * Create — or replace, when the name already exists (matched
971
+ * case-insensitively) — a saved style-guide ruleset, so later
972
+ * `styleguide(text, name)` calls check against it by name. Owned by the
973
+ * API key's account (its team, for team-scoped keys). Requires a
974
+ * per-account API key.
975
+ *
976
+ * Returns `ruleset` (the saved `{name, rules, created_at, updated_at}`)
977
+ * and `created` (false when an existing ruleset was replaced).
978
+ *
979
+ * @param {string} name - The ruleset name (the API caps it at 100
980
+ * characters; surrounding/duplicate whitespace is normalized).
981
+ * @param {Array<string|{name: string, description?: string}>} rules - The
982
+ * rules to save; same shape and bounds as styleguide()'s inline rules.
983
+ * @returns {Promise<AxiosResponse>}
984
+ */
985
+ saveStyleguideRuleset(name, rules) {
986
+ const url = `${this.urlEndpoint}styleguide/rulesets`;
987
+ if (typeof name !== 'string' || name.trim().length === 0) {
988
+ throw new TypeError('saveStyleguideRuleset: name must be a non-empty string.');
989
+ }
990
+ validateStyleguideRules(rules, 'saveStyleguideRuleset');
991
+
992
+ let data = {
993
+ name,
994
+ rules,
995
+ key: this.apiKey,
996
+ }
997
+
998
+ return this.axiosPromise(url, 'post', data);
999
+ }
1000
+
1001
+ /**
1002
+ * Delete a saved style-guide ruleset by name (matched case-insensitively;
1003
+ * 404 when no such ruleset exists). Requires a per-account API key.
1004
+ *
1005
+ * @param {string} name - The ruleset name to delete.
1006
+ * @returns {Promise<AxiosResponse>}
1007
+ */
1008
+ deleteStyleguideRuleset(name) {
1009
+ const url = `${this.urlEndpoint}styleguide/rulesets`;
1010
+ if (typeof name !== 'string' || name.trim().length === 0) {
1011
+ throw new TypeError('deleteStyleguideRuleset: name must be a non-empty string.');
1012
+ }
1013
+ // The name rides the query string: some HTTP stacks drop DELETE bodies,
1014
+ // and the API accepts the query form for exactly that reason.
1015
+ return axios({
1016
+ url,
1017
+ method: 'delete',
1018
+ params: { key: this.apiKey, name },
1019
+ timeout: this.timeout,
1020
+ });
1021
+ }
1022
+
1023
+ /**
1024
+ * Rewrite a text in plain language at a target reading level, keeping all
1025
+ * of its content, structure and language — nothing is summarized or
1026
+ * dropped.
1027
+ *
1028
+ * Returns `simplified` (the rewrite, structure preserved), the applied
1029
+ * `reading_level`, the `lang` the readability scores were computed for
1030
+ * (detected when not supplied) and, when that language supports scoring, a
1031
+ * `readability` block: `{before: {grade, ease}, after: {grade, ease}}`
1032
+ * Flesch-Kincaid scores, so the improvement is verifiable.
1033
+ *
1034
+ * @param {string|string[]} text - The text to simplify (the API caps it at
1035
+ * 5,000 characters), or an array of 1–10 texts for the batch form (sent
1036
+ * as the API's `texts` parameter; combined length up to 5,000
1037
+ * characters, same options applied to every item). A batch response is
1038
+ * `{results: [...]}` with one single-response-shaped entry per input, in
1039
+ * order; with `lang` omitted, each item's language (and readability
1040
+ * scores) is detected on its own. Sent to the model as submitted —
1041
+ * markup and line structure are preserved, not stripped.
1042
+ * @param {Object} [options]
1043
+ * @param {string} [options.reading_level] - Target audience: 'plain'
1044
+ * (plain-language style for a general audience, the API default),
1045
+ * 'elementary' (~US grade 3-5), 'middle_school' (~grade 6-8) or
1046
+ * 'high_school' (~grade 9-10).
1047
+ * @param {string[]} [options.preserve_terms] - Up to 20 terms (each up to
1048
+ * 100 characters) the rewrite must keep exactly as written — product
1049
+ * names, defined legal terms. A rewrite that drops one is refused
1050
+ * server-side rather than returned.
1051
+ * @param {string} [options.lang] - ISO 639-1 code used to pick the
1052
+ * readability formula for the before/after scores. Omit for
1053
+ * auto-detection (falls back to 'en'). The rewrite itself always stays
1054
+ * in the text's own language.
1055
+ * @returns {Promise<AxiosResponse>}
1056
+ */
1057
+ simplify(text, options = {}) {
1058
+ const url = `${this.urlEndpoint}simplify`;
1059
+
1060
+ // Shape/type checks only. Size limits (text/term length) are server
1061
+ // configuration and enforced there with a clear 400 (see classify()).
1062
+ // An array of texts is the batch form (SAP-412, sent as `texts`, one
1063
+ // result per item); the 1–10 item bound is structural like classify's.
1064
+ const isBatch = Array.isArray(text);
1065
+ if (isBatch) {
1066
+ // for...of (not text.every) so holes in a sparse array are visited as
1067
+ // undefined and rejected, rather than skipped.
1068
+ let ok = text.length >= 1 && text.length <= 10;
1069
+ if (ok) {
1070
+ for (const item of text) {
1071
+ if (typeof item !== 'string' || item.trim().length === 0) {
1072
+ ok = false;
1073
+ break;
1074
+ }
1075
+ }
1076
+ }
1077
+ if (!ok) {
1078
+ throw new TypeError('simplify: texts must be an array of 1 to 10 non-empty strings.');
1079
+ }
1080
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
1081
+ throw new TypeError('simplify: text must be a non-empty string.');
1082
+ }
1083
+
1084
+ let data = {
1085
+ ...(isBatch ? { texts: text } : { text }),
1086
+ key: this.apiKey,
1087
+ }
1088
+
1089
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
1090
+ // bad type rather than silently treated as "omitted" (same as translate's
1091
+ // option handling).
1092
+ if (options?.reading_level !== undefined) {
1093
+ if (!SIMPLIFY_READING_LEVELS.includes(options.reading_level)) {
1094
+ throw new TypeError('simplify: reading_level must be one of '
1095
+ + `${SIMPLIFY_READING_LEVELS.join(', ')}.`);
1096
+ }
1097
+ data.reading_level = options.reading_level;
1098
+ }
1099
+ if (options?.preserve_terms !== undefined) {
1100
+ const terms = options.preserve_terms;
1101
+ // An empty list is a valid no-op (callers may build it dynamically); it
1102
+ // is simply omitted from the payload below rather than rejected.
1103
+ // for...of (not terms.every) so holes in a sparse array are visited as
1104
+ // undefined and rejected, rather than skipped.
1105
+ let ok = Array.isArray(terms) && terms.length <= 20;
1106
+ if (ok) {
1107
+ for (const term of terms) {
1108
+ if (typeof term !== 'string' || term.trim().length === 0) {
1109
+ ok = false;
1110
+ break;
1111
+ }
1112
+ }
1113
+ }
1114
+ if (!ok) {
1115
+ throw new TypeError('simplify: preserve_terms must be an array of up to 20 '
1116
+ + 'non-empty strings.');
1117
+ }
1118
+ if (terms.length > 0) {
1119
+ data.preserve_terms = terms;
1120
+ }
1121
+ }
1122
+ if (options?.lang !== undefined) {
1123
+ if (typeof options.lang !== 'string' || options.lang.trim().length === 0) {
1124
+ throw new TypeError('simplify: lang must be a non-empty string.');
1125
+ }
1126
+ data.lang = options.lang;
1127
+ }
1128
+
1129
+ return this.axiosPromise(url, 'post', data);
1130
+ }
1131
+
1132
+ pii(text, options = {}) {
1133
+ const url = `${this.urlEndpoint}pii`;
1134
+
1135
+ // Shape/type checks only; the set of valid PII types is server configuration
1136
+ // and enforced there with a clear 400 (see seo()).
1137
+ if (typeof text !== 'string' || text.trim().length === 0) {
1138
+ throw new TypeError('pii: text must be a non-empty string.');
1139
+ }
1140
+
1141
+ let data = {
1142
+ text,
1143
+ key: this.apiKey,
1144
+ }
1145
+
1146
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
1147
+ // type rather than silently treated as "omitted", matching classify()/seo().
1148
+ if (options?.types !== undefined) {
1149
+ if (!Array.isArray(options.types)
1150
+ || !Array.from(options.types).every((type) => typeof type === 'string' && type.trim().length > 0)) {
1151
+ throw new TypeError('pii: types must be an array of non-empty strings.');
1152
+ }
1153
+ data.types = options.types;
1154
+ }
1155
+ if (options?.redact !== undefined) {
1156
+ if (typeof options.redact !== 'boolean') {
1157
+ throw new TypeError('pii: redact must be a boolean.');
1158
+ }
1159
+ data.redact = options.redact;
1160
+ }
1161
+
1162
+ return this.axiosPromise(url, 'post', data);
1163
+ }
1164
+
1165
+ /**
1166
+ * Named-entity recognition: people, organizations, locations, dates, times,
1167
+ * money amounts, percentages, quantities, products, and events, each with
1168
+ * character offsets into the exact text submitted (nothing is stripped or
1169
+ * normalized, so spans apply directly to your copy of the document).
1170
+ * Complements pii(): that finds checksum-validated identifiers, this finds
1171
+ * names and real-world references.
1172
+ *
1173
+ * @param {string|string[]} text - The text to analyze, or an array of 1–10
1174
+ * texts for the batch form (sent as the API's `texts` parameter; combined
1175
+ * length up to 10,000 characters, same types applied to every item). A
1176
+ * batch response is `{results: [...]}` with one single-response-shaped
1177
+ * entry per input, in order, each item's spans indexing that item's own
1178
+ * text.
1179
+ * @param {Object} [options]
1180
+ * @param {string[]} [options.types] - Entity types to recognize (subset of
1181
+ * the API's supported types). Default: all of them.
1182
+ */
1183
+ ner(text, options = {}) {
1184
+ const url = `${this.urlEndpoint}ner`;
1185
+
1186
+ // Shape/type checks only; the set of valid entity types is server
1187
+ // configuration and enforced there with a clear 400 (see pii()).
1188
+ // An array of texts is the batch form (SAP-395 series, sent as `texts`,
1189
+ // one result per item); the 1–10 item bound is structural like classify's.
1190
+ const isBatch = Array.isArray(text);
1191
+ if (isBatch) {
1192
+ // for...of (not text.every) so holes in a sparse array are visited as
1193
+ // undefined and rejected, rather than skipped.
1194
+ let ok = text.length >= 1 && text.length <= 10;
1195
+ if (ok) {
1196
+ for (const item of text) {
1197
+ if (typeof item !== 'string' || item.trim().length === 0) {
1198
+ ok = false;
1199
+ break;
1200
+ }
1201
+ }
1202
+ }
1203
+ if (!ok) {
1204
+ throw new TypeError('ner: texts must be an array of 1 to 10 non-empty strings.');
1205
+ }
1206
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
1207
+ throw new TypeError('ner: text must be a non-empty string.');
1208
+ }
1209
+
1210
+ let data = {
1211
+ ...(isBatch ? { texts: text } : { text }),
1212
+ key: this.apiKey,
1213
+ }
1214
+
1215
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
1216
+ // type rather than silently treated as "omitted", matching pii().
1217
+ // Reject an empty list too: the MCP schema requires at least one type
1218
+ // (min(1)), so an empty array is typed-valid input that would otherwise
1219
+ // become an avoidable API error.
1220
+ if (options?.types !== undefined) {
1221
+ if (!Array.isArray(options.types) || options.types.length === 0
1222
+ || !Array.from(options.types).every((type) => typeof type === 'string' && type.trim().length > 0)) {
1223
+ throw new TypeError('ner: types must be a non-empty array of non-empty strings.');
1224
+ }
1225
+ data.types = options.types;
1226
+ }
1227
+
1228
+ return this.axiosPromise(url, 'post', data);
1229
+ }
1230
+
1231
+ /**
1232
+ * Check whether an account-wide block (expired key/subscription,
1233
+ * depleted credits, billing limit, trial daily allowance, monthly quota)
1234
+ * stops this API key from making billable calls, and if so why and for
1235
+ * how long. Works while the key is blocked, which is when it is most
1236
+ * useful: call it after an unexpected 402 or monthly-quota 429 instead of
1237
+ * parsing the error text. It does not reflect a single endpoint's
1238
+ * short-term rate limit (each endpoint has its own bucket), so a
1239
+ * "Capacity used." 429 elsewhere can coexist with api_access: true — use
1240
+ * that 429's retry_after / X-RateLimit-* headers instead. Unbilled, but each call draws a fixed 2,000
1241
+ * characters from its own rate-limit bucket, so don't poll it per request.
1242
+ * Requires a per-account API key; the public demo key and MCP OAuth
1243
+ * access tokens get a 403.
1244
+ *
1245
+ * Always returns `api_access`, `blocked_reason` (null, or one of
1246
+ * key_expired, subscription_expired, credits_depleted,
1247
+ * monthly_billing_limit, trial_daily_limit, monthly_quota_exhausted),
1248
+ * `message` and `usage` ({monthly_quota_chars}); `retry_after` (seconds)
1249
+ * only when blocked. Keys that are not team-scoped also get
1250
+ * `usage.month_start`/`usage.chars_this_month`, `subscription`, `credits`
1251
+ * and, for free-trial accounts, `trial`.
1252
+ *
1253
+ * @returns {Promise<AxiosResponse>}
1254
+ */
1255
+ accountStatus() {
1256
+ const url = `${this.urlEndpoint}account/status`;
1257
+ // Not axiosPromise: this is a GET and the key rides the query string,
1258
+ // like styleguideRulesets().
1259
+ return axios({
1260
+ url,
1261
+ method: 'get',
1262
+ params: { key: this.apiKey },
1263
+ timeout: this.timeout,
1264
+ });
1265
+ }
211
1266
  }